diff --git a/.github/workflows/production_pipeline.yml b/.github/workflows/production_pipeline.yml index 23bb69c00..82f37e743 100644 --- a/.github/workflows/production_pipeline.yml +++ b/.github/workflows/production_pipeline.yml @@ -42,6 +42,7 @@ jobs: DB_USERNAME: postgres DB_PASSWORD: password RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + PARALLEL_TEST_PROCESSORS: 4 steps: - name: Get latest release with tag @@ -71,7 +72,7 @@ jobs: - name: Create database run: | - bundle exec rake db:prepare + bundle exec rake parallel:setup - name: Compile Assets run: | @@ -79,7 +80,7 @@ jobs: - name: Run tests run: | - bundle exec rspec --exclude-pattern "features/*" --fail-fast + bundle exec rake parallel:spec['spec\/(?!features)'] feature_test: name: Feature Tests diff --git a/.github/workflows/review_pipeline.yml b/.github/workflows/review_pipeline.yml index 5656898bc..ac883c788 100644 --- a/.github/workflows/review_pipeline.yml +++ b/.github/workflows/review_pipeline.yml @@ -123,7 +123,7 @@ jobs: cf set-env $APP_NAME IMPORT_PAAS_INSTANCE $IMPORT_PAAS_INSTANCE cf set-env $APP_NAME EXPORT_PAAS_INSTANCE "dluhc-core-review-export-bucket" cf set-env $APP_NAME S3_CONFIG $S3_CONFIG - cf set-env $APP_NAME CSV_DOWNLOAD_PAAS_INSTANCE "dluhc-core-review-csv-bucket" + cf set-env $APP_NAME CSV_DOWNLOAD_PAAS_INSTANCE "dluhc-core-staging-csv-bucket" cf set-env $APP_NAME SENTRY_DSN $SENTRY_DSN cf set-env $APP_NAME APP_HOST "https://dluhc-core-review-${{ github.event.pull_request.number }}.london.cloudapps.digital" diff --git a/.github/workflows/staging_pipeline.yml b/.github/workflows/staging_pipeline.yml index 8d48f7b7e..9ca8cb69d 100644 --- a/.github/workflows/staging_pipeline.yml +++ b/.github/workflows/staging_pipeline.yml @@ -45,6 +45,7 @@ jobs: DB_USERNAME: postgres DB_PASSWORD: password RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + PARALLEL_TEST_PROCESSORS: 4 steps: - name: Checkout @@ -63,7 +64,7 @@ jobs: - name: Create database run: | - bundle exec rake db:prepare + bundle exec rake parallel:setup - name: Compile assets run: | @@ -71,7 +72,7 @@ jobs: - name: Run tests run: | - bundle exec rspec --exclude-pattern "features/*" --fail-fast + bundle exec rake parallel:spec['spec\/(?!features)'] feature_test: name: Feature Tests diff --git a/Gemfile.lock b/Gemfile.lock index b9b16ab1e..9083ba67e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,67 +13,67 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.4.2) - actionpack (= 7.0.4.2) - activesupport (= 7.0.4.2) + actioncable (7.0.4.3) + actionpack (= 7.0.4.3) + activesupport (= 7.0.4.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.4.2) - actionpack (= 7.0.4.2) - activejob (= 7.0.4.2) - activerecord (= 7.0.4.2) - activestorage (= 7.0.4.2) - activesupport (= 7.0.4.2) + actionmailbox (7.0.4.3) + actionpack (= 7.0.4.3) + activejob (= 7.0.4.3) + activerecord (= 7.0.4.3) + activestorage (= 7.0.4.3) + activesupport (= 7.0.4.3) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.4.2) - actionpack (= 7.0.4.2) - actionview (= 7.0.4.2) - activejob (= 7.0.4.2) - activesupport (= 7.0.4.2) + actionmailer (7.0.4.3) + actionpack (= 7.0.4.3) + actionview (= 7.0.4.3) + activejob (= 7.0.4.3) + activesupport (= 7.0.4.3) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.4.2) - actionview (= 7.0.4.2) - activesupport (= 7.0.4.2) + actionpack (7.0.4.3) + actionview (= 7.0.4.3) + activesupport (= 7.0.4.3) rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.4.2) - actionpack (= 7.0.4.2) - activerecord (= 7.0.4.2) - activestorage (= 7.0.4.2) - activesupport (= 7.0.4.2) + actiontext (7.0.4.3) + actionpack (= 7.0.4.3) + activerecord (= 7.0.4.3) + activestorage (= 7.0.4.3) + activesupport (= 7.0.4.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.4.2) - activesupport (= 7.0.4.2) + actionview (7.0.4.3) + activesupport (= 7.0.4.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.4.2) - activesupport (= 7.0.4.2) + activejob (7.0.4.3) + activesupport (= 7.0.4.3) globalid (>= 0.3.6) - activemodel (7.0.4.2) - activesupport (= 7.0.4.2) - activerecord (7.0.4.2) - activemodel (= 7.0.4.2) - activesupport (= 7.0.4.2) - activestorage (7.0.4.2) - actionpack (= 7.0.4.2) - activejob (= 7.0.4.2) - activerecord (= 7.0.4.2) - activesupport (= 7.0.4.2) + activemodel (7.0.4.3) + activesupport (= 7.0.4.3) + activerecord (7.0.4.3) + activemodel (= 7.0.4.3) + activesupport (= 7.0.4.3) + activestorage (7.0.4.3) + actionpack (= 7.0.4.3) + activejob (= 7.0.4.3) + activerecord (= 7.0.4.3) + activesupport (= 7.0.4.3) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.4.2) + activesupport (7.0.4.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -134,7 +134,7 @@ GEM launchy childprocess (4.1.0) coderay (1.1.3) - concurrent-ruby (1.2.0) + concurrent-ruby (1.2.2) connection_pool (2.3.0) crack (0.4.5) rexml @@ -221,7 +221,7 @@ GEM matrix (0.4.2) method_source (1.0.0) mini_mime (1.1.2) - minitest (5.17.0) + minitest (5.18.0) msgpack (1.6.0) net-imap (0.3.4) date @@ -279,35 +279,35 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.6.2) - rack (2.2.6.3) + rack (2.2.6.4) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-mini-profiler (2.3.4) rack (>= 1.2.0) - rack-test (2.0.2) + rack-test (2.1.0) rack (>= 1.3) - rails (7.0.4.2) - actioncable (= 7.0.4.2) - actionmailbox (= 7.0.4.2) - actionmailer (= 7.0.4.2) - actionpack (= 7.0.4.2) - actiontext (= 7.0.4.2) - actionview (= 7.0.4.2) - activejob (= 7.0.4.2) - activemodel (= 7.0.4.2) - activerecord (= 7.0.4.2) - activestorage (= 7.0.4.2) - activesupport (= 7.0.4.2) + rails (7.0.4.3) + actioncable (= 7.0.4.3) + actionmailbox (= 7.0.4.3) + actionmailer (= 7.0.4.3) + actionpack (= 7.0.4.3) + actiontext (= 7.0.4.3) + actionview (= 7.0.4.3) + activejob (= 7.0.4.3) + activemodel (= 7.0.4.3) + activerecord (= 7.0.4.3) + activestorage (= 7.0.4.3) + activesupport (= 7.0.4.3) bundler (>= 1.15.0) - railties (= 7.0.4.2) + railties (= 7.0.4.3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.5.0) loofah (~> 2.19, >= 2.19.1) - railties (7.0.4.2) - actionpack (= 7.0.4.2) - activesupport (= 7.0.4.2) + railties (7.0.4.3) + actionpack (= 7.0.4.3) + activesupport (= 7.0.4.3) method_source rake (>= 12.2) thor (~> 1.0) diff --git a/app/components/bulk_upload_error_row_component.rb b/app/components/bulk_upload_error_row_component.rb index e19717954..13ac326ef 100644 --- a/app/components/bulk_upload_error_row_component.rb +++ b/app/components/bulk_upload_error_row_component.rb @@ -24,14 +24,7 @@ class BulkUploadErrorRowComponent < ViewComponent::Base end def question_for_field(field) - case bulk_upload.log_type - when "lettings" - BulkUpload::Lettings::Validator.question_for_field(field.to_sym) - when "sales" - BulkUpload::Sales::Validator.question_for_field(field.to_sym) - else - "Unknown question" - end + bulk_upload.prefix_namespace::RowParser.question_for_field(field.to_sym) end def bulk_upload diff --git a/app/components/bulk_upload_error_summary_table_component.html.erb b/app/components/bulk_upload_error_summary_table_component.html.erb index f1749ee92..9cf146dc0 100644 --- a/app/components/bulk_upload_error_summary_table_component.html.erb +++ b/app/components/bulk_upload_error_summary_table_component.html.erb @@ -16,7 +16,7 @@ <% body.row do |row| %> <% row.cell(text: error[0][0]) %> <% row.cell(text: error[1]) %> - <% row.cell(text: BulkUpload::Lettings::Validator.question_for_field(error[0][1].to_sym)) %> + <% row.cell(text: question_for_field(error[0][1].to_sym)) %> <% row.cell(text: error[0][2]) %> <% row.cell(text: error[0][1]) %> <% end %> diff --git a/app/components/bulk_upload_error_summary_table_component.rb b/app/components/bulk_upload_error_summary_table_component.rb index a3d295110..909fb5f0d 100644 --- a/app/components/bulk_upload_error_summary_table_component.rb +++ b/app/components/bulk_upload_error_summary_table_component.rb @@ -3,6 +3,8 @@ class BulkUploadErrorSummaryTableComponent < ViewComponent::Base attr_reader :bulk_upload + delegate :question_for_field, to: :row_parser_class + def initialize(bulk_upload:) @bulk_upload = bulk_upload @@ -27,4 +29,8 @@ private def display_threshold DISPLAY_THRESHOLD end + + def row_parser_class + bulk_upload.prefix_namespace::RowParser + end end diff --git a/app/controllers/bulk_upload_lettings_logs_controller.rb b/app/controllers/bulk_upload_lettings_logs_controller.rb index 9b37cd2b5..ec5975432 100644 --- a/app/controllers/bulk_upload_lettings_logs_controller.rb +++ b/app/controllers/bulk_upload_lettings_logs_controller.rb @@ -33,6 +33,8 @@ private end def in_crossover_period? + return true if FeatureToggle.force_crossover? + FormHandler.instance.lettings_in_crossover_period? end diff --git a/app/controllers/lettings_logs_controller.rb b/app/controllers/lettings_logs_controller.rb index f86a55a27..e07878b0b 100644 --- a/app/controllers/lettings_logs_controller.rb +++ b/app/controllers/lettings_logs_controller.rb @@ -2,10 +2,16 @@ class LettingsLogsController < LogsController before_action :find_resource, except: %i[create index edit] before_action :session_filters, if: :current_user, only: %i[index email_csv download_csv] before_action :set_session_filters, if: :current_user, only: %i[index email_csv download_csv] + before_action :authenticate_scope!, only: %i[download_csv email_csv] before_action :extract_bulk_upload_from_session_filters, only: [:index] before_action :redirect_if_bulk_upload_resolved, only: [:index] + def authenticate_scope! + codes_only_export = codes_only_export?(params) + head :unauthorized and return unless current_user.support? || !codes_only_export + end + def index respond_to do |format| format.html do @@ -80,13 +86,19 @@ class LettingsLogsController < LogsController def download_csv unpaginated_filtered_logs = filtered_logs(current_user.lettings_logs, search_term, @session_filters) + codes_only = codes_only_export?(params) + + render "download_csv", locals: { search_term:, count: unpaginated_filtered_logs.size, post_path: email_csv_lettings_logs_path, codes_only: } + end - render "download_csv", locals: { search_term:, count: unpaginated_filtered_logs.size, post_path: email_csv_lettings_logs_path } + def codes_only_export?(params) + params.require(:codes_only) == "true" end def email_csv all_orgs = params["organisation_select"] == "all" - EmailCsvJob.perform_later(current_user, search_term, @session_filters, all_orgs) + codes_only_export = params.require(:codes_only) == "true" + EmailCsvJob.perform_later(current_user, search_term, @session_filters, all_orgs, nil, codes_only_export) redirect_to csv_confirmation_lettings_logs_path end diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 211283ffe..418526960 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -107,12 +107,14 @@ class OrganisationsController < ApplicationController def download_csv organisation_logs = LettingsLog.all.where(owning_organisation_id: @organisation.id) unpaginated_filtered_logs = filtered_logs(organisation_logs, search_term, @session_filters) + codes_only = params.require(:codes_only) == "true" - render "logs/download_csv", locals: { search_term:, count: unpaginated_filtered_logs.size, post_path: logs_email_csv_organisation_path } + render "logs/download_csv", locals: { search_term:, count: unpaginated_filtered_logs.size, post_path: logs_email_csv_organisation_path, codes_only: } end def email_csv - EmailCsvJob.perform_later(current_user, search_term, @session_filters, false, @organisation) + codes_only_export = params.require(:codes_only) == "true" + EmailCsvJob.perform_later(current_user, search_term, @session_filters, false, @organisation, codes_only_export) redirect_to logs_csv_confirmation_organisation_path end diff --git a/app/controllers/start_controller.rb b/app/controllers/start_controller.rb index d09934cbc..d707d3e91 100644 --- a/app/controllers/start_controller.rb +++ b/app/controllers/start_controller.rb @@ -4,4 +4,28 @@ class StartController < ApplicationController redirect_to(lettings_logs_path) end end + + def download_23_24_sales_form + send_file( + Rails.root.join("public/files/2023_24_sales_paper_form.pdf"), + filename: "2023-24 Sales paper form.pdf", + type: "application/pdf", + ) + end + + def download_22_23_sales_form + send_file( + Rails.root.join("public/files/2022_23_sales_paper_form.pdf"), + filename: "2022-23 Sales paper form.pdf", + type: "application/pdf", + ) + end + + def download_23_24_lettings_form + send_file( + Rails.root.join("public/files/2023_24_lettings_paper_form.pdf"), + filename: "2023-24 Lettings paper form.pdf", + type: "application/pdf", + ) + end end diff --git a/app/helpers/logs_helper.rb b/app/helpers/logs_helper.rb index 7653b7e5c..aa132afd6 100644 --- a/app/helpers/logs_helper.rb +++ b/app/helpers/logs_helper.rb @@ -33,10 +33,9 @@ module LogsHelper 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"]) + def csv_download_url_for_controller(controller_type:, search:, codes_only:) + case log_type_for_controller(controller_type) + when "lettings" then csv_download_lettings_logs_path(search:, codes_only:) end end end diff --git a/app/jobs/email_csv_job.rb b/app/jobs/email_csv_job.rb index c7009645f..6e7888f18 100644 --- a/app/jobs/email_csv_job.rb +++ b/app/jobs/email_csv_job.rb @@ -5,14 +5,14 @@ class EmailCsvJob < ApplicationJob EXPIRATION_TIME = 3.hours.to_i - def perform(user, search_term = nil, filters = {}, all_orgs = false, organisation = nil) # rubocop:disable Style/OptionalBooleanParameter - sidekiq can't serialise named params + def perform(user, search_term = nil, filters = {}, all_orgs = false, organisation = nil, codes_only_export = false) # rubocop:disable Style/OptionalBooleanParameter - sidekiq can't serialise named params unfiltered_logs = organisation.present? && user.support? ? LettingsLog.where(owning_organisation_id: organisation.id) : user.lettings_logs filtered_logs = FilterService.filter_logs(unfiltered_logs, search_term, filters, all_orgs, user) filename = organisation.present? ? "logs-#{organisation.name}-#{Time.zone.now}.csv" : "logs-#{Time.zone.now}.csv" storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["CSV_DOWNLOAD_PAAS_INSTANCE"]) - storage_service.write_file(filename, BYTE_ORDER_MARK + filtered_logs.to_csv(user)) + storage_service.write_file(filename, BYTE_ORDER_MARK + filtered_logs.to_csv(user, codes_only_export:)) url = storage_service.get_presigned_url(filename, EXPIRATION_TIME) diff --git a/app/mailers/bulk_upload_mailer.rb b/app/mailers/bulk_upload_mailer.rb index fe1e81517..ebc9df7a6 100644 --- a/app/mailers/bulk_upload_mailer.rb +++ b/app/mailers/bulk_upload_mailer.rb @@ -73,11 +73,7 @@ class BulkUploadMailer < NotifyMailer start_bulk_upload_sales_logs_url end - validator_class = if bulk_upload.lettings? - BulkUpload::Lettings::Validator - else - BulkUpload::Sales::Validator - end + row_parser_class = bulk_upload.prefix_namespace::RowParser errors = bulk_upload .bulk_upload_errors @@ -87,7 +83,7 @@ class BulkUploadMailer < NotifyMailer .keys .sort_by { |_col, field| field } .map do |col, field| - "- Column #{col} (#{validator_class.question_for_field(field.to_sym)})" + "- Column #{col} (#{row_parser_class.question_for_field(field.to_sym)})" end send_email( @@ -104,7 +100,7 @@ class BulkUploadMailer < NotifyMailer ) end - def send_bulk_upload_failed_service_error_mail(bulk_upload:) + def send_bulk_upload_failed_service_error_mail(bulk_upload:, errors: []) bulk_upload_link = if bulk_upload.lettings? start_bulk_upload_lettings_logs_url else @@ -119,6 +115,7 @@ class BulkUploadMailer < NotifyMailer upload_timestamp: bulk_upload.created_at, lettings_or_sales: bulk_upload.log_type, year_combo: bulk_upload.year_combo, + errors: errors.map { |e| "- #{e}" }.join("\n"), bulk_upload_link:, }, ) diff --git a/app/models/bulk_upload.rb b/app/models/bulk_upload.rb index 0adf0d84a..ff334b968 100644 --- a/app/models/bulk_upload.rb +++ b/app/models/bulk_upload.rb @@ -46,6 +46,28 @@ class BulkUpload < ApplicationRecord needstype == 2 end + def prefix_namespace + type_class = case log_type + when "lettings" + "Lettings" + when "sales" + "Sales" + else + raise "unknown log type" + end + + year_class = case year + when 2022 + "Year2022" + when 2023 + "Year2023" + else + raise "unknown year" + end + + "BulkUpload::#{type_class}::#{year_class}".constantize + end + private def generate_identifier diff --git a/app/models/form/lettings/pages/lead_tenant_nationality.rb b/app/models/form/lettings/pages/lead_tenant_nationality.rb index ae67b1e32..09a25286c 100644 --- a/app/models/form/lettings/pages/lead_tenant_nationality.rb +++ b/app/models/form/lettings/pages/lead_tenant_nationality.rb @@ -6,6 +6,6 @@ class Form::Lettings::Pages::LeadTenantNationality < ::Form::Page end def questions - @questions ||= [Form::Lettings::Questions::National.new(nil, nil, self)] + @questions ||= [Form::Lettings::Questions::Nationality.new(nil, nil, self)] end end diff --git a/app/models/form/lettings/pages/max_rent_value_check.rb b/app/models/form/lettings/pages/max_rent_value_check.rb index 88d0561bf..59b2d14fe 100644 --- a/app/models/form/lettings/pages/max_rent_value_check.rb +++ b/app/models/form/lettings/pages/max_rent_value_check.rb @@ -4,11 +4,11 @@ class Form::Lettings::Pages::MaxRentValueCheck < ::Form::Page @id = "max_rent_value_check" @depends_on = [{ "rent_in_soft_max_range?" => true }] @title_text = { - "translation" => "soft_validations.rent.max.title_text", + "translation" => "soft_validations.rent.outside_range_title", "arguments" => [{ "key" => "brent", "label" => true, "i18n_template" => "brent" }], } @informative_text = { - "translation" => "soft_validations.rent.max.hint_text", + "translation" => "soft_validations.rent.max_hint_text", "arguments" => [ { "key" => "soft_max_for_period", diff --git a/app/models/form/lettings/pages/min_rent_value_check.rb b/app/models/form/lettings/pages/min_rent_value_check.rb index 3bcb95789..ed5ede781 100644 --- a/app/models/form/lettings/pages/min_rent_value_check.rb +++ b/app/models/form/lettings/pages/min_rent_value_check.rb @@ -4,11 +4,11 @@ class Form::Lettings::Pages::MinRentValueCheck < ::Form::Page @id = "min_rent_value_check" @depends_on = [{ "rent_in_soft_min_range?" => true }] @title_text = { - "translation" => "soft_validations.rent.min.title_text", + "translation" => "soft_validations.rent.outside_range_title", "arguments" => [{ "key" => "brent", "label" => true, "i18n_template" => "brent" }], } @informative_text = { - "translation" => "soft_validations.rent.min.hint_text", + "translation" => "soft_validations.rent.min_hint_text", "arguments" => [ { "key" => "soft_min_for_period", diff --git a/app/models/form/lettings/pages/starter_tenancy_type.rb b/app/models/form/lettings/pages/starter_tenancy_type.rb index c488b7b17..fe147bc77 100644 --- a/app/models/form/lettings/pages/starter_tenancy_type.rb +++ b/app/models/form/lettings/pages/starter_tenancy_type.rb @@ -2,13 +2,13 @@ class Form::Lettings::Pages::StarterTenancyType < ::Form::Page def initialize(id, hsh, subsection) super @id = "starter_tenancy_type" - @depends_on = [{ "startertenancy" => 1 }] + @depends_on = [{ "starter_tenancy?" => true }] end def questions @questions ||= [ - Form::Lettings::Questions::StarterTenancy.new(nil, nil, self), - Form::Lettings::Questions::Tenancyother.new(nil, nil, self), + Form::Lettings::Questions::StarterTenancyType.new(nil, nil, self), + Form::Lettings::Questions::TenancyOther.new(nil, nil, self), ] end end diff --git a/app/models/form/lettings/pages/tenancy_length.rb b/app/models/form/lettings/pages/tenancy_length.rb index 943ea2bc9..cb533dcf9 100644 --- a/app/models/form/lettings/pages/tenancy_length.rb +++ b/app/models/form/lettings/pages/tenancy_length.rb @@ -2,10 +2,10 @@ class Form::Lettings::Pages::TenancyLength < ::Form::Page def initialize(id, hsh, subsection) super @id = "tenancy_length" - @depends_on = [{ "tenancy" => 4 }, { "tenancy" => 6 }, { "tenancy" => 3 }] + @depends_on = [{ "tenancy_type_fixed_term?" => true }] end def questions - @questions ||= [Form::Lettings::Questions::Tenancylength.new(nil, nil, self)] + @questions ||= [Form::Lettings::Questions::TenancyLength.new(nil, nil, self)] end end diff --git a/app/models/form/lettings/pages/tenancy_type.rb b/app/models/form/lettings/pages/tenancy_type.rb index 8ee6c3bbc..c15f6f27a 100644 --- a/app/models/form/lettings/pages/tenancy_type.rb +++ b/app/models/form/lettings/pages/tenancy_type.rb @@ -2,13 +2,13 @@ class Form::Lettings::Pages::TenancyType < ::Form::Page def initialize(id, hsh, subsection) super @id = "tenancy_type" - @depends_on = [{ "startertenancy" => 2 }] + @depends_on = [{ "starter_tenancy?" => false }] end def questions @questions ||= [ - Form::Lettings::Questions::Tenancy.new(nil, nil, self), - Form::Lettings::Questions::Tenancyother.new(nil, nil, self), + Form::Lettings::Questions::TenancyType.new(nil, nil, self), + Form::Lettings::Questions::TenancyOther.new(nil, nil, self), ] end end diff --git a/app/models/form/lettings/questions/national.rb b/app/models/form/lettings/questions/nationality.rb similarity index 78% rename from app/models/form/lettings/questions/national.rb rename to app/models/form/lettings/questions/nationality.rb index 1e0b4a3d7..c86f93b09 100644 --- a/app/models/form/lettings/questions/national.rb +++ b/app/models/form/lettings/questions/nationality.rb @@ -1,9 +1,9 @@ -class Form::Lettings::Questions::National < ::Form::Question +class Form::Lettings::Questions::Nationality < ::Form::Question def initialize(id, hsh, page) super @id = "national" @check_answer_label = "Lead tenant’s nationality" - @header = "What is the lead tenant’s nationality?" + @header = "What is the nationality of the lead tenant?" @type = "radio" @check_answers_card_number = 1 @hint_text = "The lead tenant is the person in the household who does the most paid work. If several people do the same paid work, the lead tenant is whoever is the oldest." @@ -15,6 +15,8 @@ class Form::Lettings::Questions::National < ::Form::Question "18" => { "value" => "United Kingdom" }, "17" => { "value" => "Republic of Ireland" }, "19" => { "value" => "European Economic Area (EEA) country, excluding Ireland" }, + "20" => { "value" => "Afghanistan" }, + "21" => { "value" => "Ukraine" }, "12" => { "value" => "Other" }, "divider" => true, "13" => { "value" => "Tenant prefers not to say" }, diff --git a/app/models/form/lettings/questions/reason.rb b/app/models/form/lettings/questions/reason.rb index 3e5d78944..9598780ec 100644 --- a/app/models/form/lettings/questions/reason.rb +++ b/app/models/form/lettings/questions/reason.rb @@ -59,8 +59,11 @@ class Form::Lettings::Questions::Reason < ::Form::Question "17" => { "value" => "To move nearer to work", }, - "7" => { - "value" => "Domestic abuse", + "48" => { + "value" => "Domestic abuse - previously joint tenancy with partner", + }, + "49" => { + "value" => "Domestic abuse - other", }, "31" => { "value" => "Hate crime", diff --git a/app/models/form/lettings/questions/starter_tenancy.rb b/app/models/form/lettings/questions/starter_tenancy.rb index 6cc86c02d..e69de29bb 100644 --- a/app/models/form/lettings/questions/starter_tenancy.rb +++ b/app/models/form/lettings/questions/starter_tenancy.rb @@ -1,23 +0,0 @@ -class Form::Lettings::Questions::StarterTenancy < ::Form::Question - def initialize(id, hsh, page) - super - @id = "tenancy" - @check_answer_label = "Type of main tenancy after the starter period has ended?" - @header = "What is the type of tenancy after the starter period has ended?" - @type = "radio" - @check_answers_card_number = 0 - @hint_text = "This is also known as an ‘introductory period’." - @answer_options = ANSWER_OPTIONS - @conditional_for = { "tenancyother" => [3] } - @question_number = 27 - end - - ANSWER_OPTIONS = { - "4" => { "value" => "Assured Shorthold Tenancy (AST) – Fixed term" }, - "6" => { "value" => "Secure – fixed term" }, - "2" => { "value" => "Assured – lifetime" }, - "7" => { "value" => "Secure – lifetime" }, - "5" => { "value" => "Licence agreement" }, - "3" => { "value" => "Other" }, - }.freeze -end diff --git a/app/models/form/lettings/questions/starter_tenancy_type.rb b/app/models/form/lettings/questions/starter_tenancy_type.rb new file mode 100644 index 000000000..13fa20ef6 --- /dev/null +++ b/app/models/form/lettings/questions/starter_tenancy_type.rb @@ -0,0 +1,37 @@ +class Form::Lettings::Questions::StarterTenancyType < ::Form::Question + def initialize(id, hsh, page) + super + @id = "tenancy" + @check_answer_label = "Type of main tenancy after the starter period has ended?" + @header = "What is the type of tenancy after the starter period has ended?" + @type = "radio" + @check_answers_card_number = 0 + @hint_text = "This is also known as an ‘introductory period’." + @answer_options = ANSWER_OPTIONS + @conditional_for = { "tenancyother" => [3] } + end + + ANSWER_OPTIONS = { + "4" => { + "value" => "Assured Shorthold Tenancy (AST) – Fixed term", + "hint" => "Mostly housing associations provide these. Fixed term tenancies are intended to be for a set amount of time up to 20 years.", + }, + "6" => { + "value" => "Secure – fixed term", + "hint" => "Mostly local authorities provide these. Fixed term tenancies are intended to be for a set amount of time up to 20 years.", + }, + "2" => { + "value" => "Assured – lifetime", + }, + "7" => { + "value" => "Secure – lifetime", + }, + "5" => { + "value" => "Licence agreement", + "hint" => "Licence agreements are mostly used for Supported Housing and work on a rolling basis.", + }, + "3" => { + "value" => "Other", + }, + }.freeze +end diff --git a/app/models/form/lettings/questions/tenancy.rb b/app/models/form/lettings/questions/tenancy.rb index c8b5f9f35..e69de29bb 100644 --- a/app/models/form/lettings/questions/tenancy.rb +++ b/app/models/form/lettings/questions/tenancy.rb @@ -1,23 +0,0 @@ -class Form::Lettings::Questions::Tenancy < ::Form::Question - def initialize(id, hsh, page) - super - @id = "tenancy" - @check_answer_label = "Type of main tenancy" - @header = "What is the type of tenancy?" - @type = "radio" - @check_answers_card_number = 0 - @hint_text = "" - @answer_options = ANSWER_OPTIONS - @conditional_for = { "tenancyother" => [3] } - @question_number = 27 - end - - ANSWER_OPTIONS = { - "4" => { "value" => "Assured Shorthold Tenancy (AST) – Fixed term" }, - "6" => { "value" => "Secure – fixed term" }, - "2" => { "value" => "Assured – lifetime" }, - "7" => { "value" => "Secure – lifetime" }, - "5" => { "value" => "Licence agreement" }, - "3" => { "value" => "Other" }, - }.freeze -end diff --git a/app/models/form/lettings/questions/tenancylength.rb b/app/models/form/lettings/questions/tenancy_length.rb similarity index 87% rename from app/models/form/lettings/questions/tenancylength.rb rename to app/models/form/lettings/questions/tenancy_length.rb index 53fc93c4b..90b34aa62 100644 --- a/app/models/form/lettings/questions/tenancylength.rb +++ b/app/models/form/lettings/questions/tenancy_length.rb @@ -1,4 +1,4 @@ -class Form::Lettings::Questions::Tenancylength < ::Form::Question +class Form::Lettings::Questions::TenancyLength < ::Form::Question def initialize(id, hsh, page) super @id = "tenancylength" diff --git a/app/models/form/lettings/questions/tenancyother.rb b/app/models/form/lettings/questions/tenancy_other.rb similarity index 79% rename from app/models/form/lettings/questions/tenancyother.rb rename to app/models/form/lettings/questions/tenancy_other.rb index e011cdb11..c19b03703 100644 --- a/app/models/form/lettings/questions/tenancyother.rb +++ b/app/models/form/lettings/questions/tenancy_other.rb @@ -1,4 +1,4 @@ -class Form::Lettings::Questions::Tenancyother < ::Form::Question +class Form::Lettings::Questions::TenancyOther < ::Form::Question def initialize(id, hsh, page) super @id = "tenancyother" diff --git a/app/models/form/lettings/questions/tenancy_type.rb b/app/models/form/lettings/questions/tenancy_type.rb new file mode 100644 index 000000000..c05b4349d --- /dev/null +++ b/app/models/form/lettings/questions/tenancy_type.rb @@ -0,0 +1,37 @@ +class Form::Lettings::Questions::TenancyType < ::Form::Question + def initialize(id, hsh, page) + super + @id = "tenancy" + @check_answer_label = "Type of main tenancy" + @header = "What is the type of tenancy?" + @type = "radio" + @check_answers_card_number = 0 + @hint_text = "" + @answer_options = ANSWER_OPTIONS + @conditional_for = { "tenancyother" => [3] } + end + + ANSWER_OPTIONS = { + "4" => { + "value" => "Assured Shorthold Tenancy (AST) – Fixed term", + "hint" => "Mostly housing associations provide these. Fixed term tenancies are intended to be for a set amount of time up to 20 years.", + }, + "6" => { + "value" => "Secure – fixed term", + "hint" => "Mostly local authorities provide these. Fixed term tenancies are intended to be for a set amount of time up to 20 years.", + }, + "2" => { + "value" => "Assured – lifetime", + }, + "7" => { + "value" => "Secure – lifetime", + }, + "5" => { + "value" => "Licence agreement", + "hint" => "Licence agreements are mostly used for Supported Housing and work on a rolling basis.", + }, + "3" => { + "value" => "Other", + }, + }.freeze +end diff --git a/app/models/form/sales/pages/buyer2_live_in_property.rb b/app/models/form/sales/pages/buyer2_live_in_property.rb index 9ba67080b..3c0ce7b3b 100644 --- a/app/models/form/sales/pages/buyer2_live_in_property.rb +++ b/app/models/form/sales/pages/buyer2_live_in_property.rb @@ -4,12 +4,12 @@ class Form::Sales::Pages::Buyer2LiveInProperty < ::Form::Page @id = "buyer_2_live_in_property" @depends_on = [ { - "jointpur" => 1, + "joint_purchase?" => true, "privacynotice" => 1, }, { - "jointpur" => 1, - "noint" => 1, + "joint_purchase?" => true, + "buyer_not_interviewed?" => true, }, ] end diff --git a/app/models/form/sales/pages/buyer2_living_in.rb b/app/models/form/sales/pages/buyer2_living_in.rb new file mode 100644 index 000000000..b8d32ffdc --- /dev/null +++ b/app/models/form/sales/pages/buyer2_living_in.rb @@ -0,0 +1,11 @@ +class Form::Sales::Pages::Buyer2LivingIn < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_living_in" + @depends_on = [{ "buyer_two_will_live_in_property?" => true }] + end + + def questions + @questions = [Form::Sales::Questions::Buyer2LivingIn.new(nil, nil, self)] + end +end diff --git a/app/models/form/sales/pages/buyer2_nationality.rb b/app/models/form/sales/pages/buyer2_nationality.rb new file mode 100644 index 000000000..3771fe331 --- /dev/null +++ b/app/models/form/sales/pages/buyer2_nationality.rb @@ -0,0 +1,11 @@ +class Form::Sales::Pages::Buyer2Nationality < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_nationality" + @depends_on = [{ "joint_purchase?" => true }] + end + + def questions + @questions ||= [Form::Sales::Questions::Buyer2Nationality.new(nil, nil, self)] + end +end diff --git a/app/models/form/sales/pages/buyer2_previous_housing_situation.rb b/app/models/form/sales/pages/buyer2_previous_housing_situation.rb new file mode 100644 index 000000000..c6a511243 --- /dev/null +++ b/app/models/form/sales/pages/buyer2_previous_housing_situation.rb @@ -0,0 +1,11 @@ +class Form::Sales::Pages::Buyer2PreviousHousingSituation < ::Form::Page + def initialize(id, hsh, subsection) + super + @id = "buyer_2_previous_housing_situation" + @depends_on = [{ "buyer_two_not_already_living_in_property?" => true }] + end + + def questions + @questions = [Form::Sales::Questions::PreviousTenureBuyer2.new(nil, nil, self)] + end +end diff --git a/app/models/form/sales/pages/nationality1.rb b/app/models/form/sales/pages/nationality1.rb index 96723e857..8db4a64ea 100644 --- a/app/models/form/sales/pages/nationality1.rb +++ b/app/models/form/sales/pages/nationality1.rb @@ -7,15 +7,12 @@ class Form::Sales::Pages::Nationality1 < ::Form::Page "privacynotice" => 1, }, { - "noint" => 1, + "buyer_not_interviewed?" => true, }, ] end def questions - @questions ||= [ - Form::Sales::Questions::Nationality1.new(nil, nil, self), - Form::Sales::Questions::OtherNationality1.new(nil, nil, self), - ] + @questions ||= [Form::Sales::Questions::Nationality1.new(nil, nil, self)] end end diff --git a/app/models/form/sales/questions/buyer1_previous_tenure.rb b/app/models/form/sales/questions/buyer1_previous_tenure.rb index 8c4f88f97..6341bee8f 100644 --- a/app/models/form/sales/questions/buyer1_previous_tenure.rb +++ b/app/models/form/sales/questions/buyer1_previous_tenure.rb @@ -11,13 +11,13 @@ class Form::Sales::Questions::Buyer1PreviousTenure < ::Form::Question def answer_options { - "1" => { "value" => "Local Authority" }, + "1" => { "value" => "Local authority tenant" }, "2" => { "value" => "Private registered provider or housing association tenant" }, "3" => { "value" => "Private tenant" }, "5" => { "value" => "Owner occupier" }, "4" => { "value" => "Tied home or renting with job" }, "6" => { "value" => "Living with family or friends" }, - "7" => { "value" => "Temporary accomodation" }, + "7" => { "value" => "Temporary accommodation" }, "9" => { "value" => "Other" }, "0" => { "value" => "Don’t know" }, } diff --git a/app/models/form/sales/questions/buyer2_living_in.rb b/app/models/form/sales/questions/buyer2_living_in.rb new file mode 100644 index 000000000..1494e61a2 --- /dev/null +++ b/app/models/form/sales/questions/buyer2_living_in.rb @@ -0,0 +1,17 @@ +class Form::Sales::Questions::Buyer2LivingIn < ::Form::Question + def initialize(id, hsh, page) + super + @id = "buy2living" + @check_answer_label = "Buyer 2 living at the same address" + @header = "At the time of purchase, was buyer 2 living at the same address as buyer 1?" + @type = "radio" + @hint_text = "" + @answer_options = ANSWER_OPTIONS + end + + ANSWER_OPTIONS = { + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + "3" => { "value" => "Don't know" }, + }.freeze +end diff --git a/app/models/form/sales/questions/buyer2_nationality.rb b/app/models/form/sales/questions/buyer2_nationality.rb new file mode 100644 index 000000000..d2056822e --- /dev/null +++ b/app/models/form/sales/questions/buyer2_nationality.rb @@ -0,0 +1,26 @@ +class Form::Sales::Questions::Buyer2Nationality < ::Form::Question + def initialize(id, hsh, page) + super + @id = "nationalbuy2" + @check_answer_label = "Buyer 2’s nationality" + @header = "What is buyer 2’s nationality?" + @type = "radio" + @answer_options = ANSWER_OPTIONS + @check_answers_card_number = 2 + @question_number = 32 + @inferred_check_answers_value = [{ + "condition" => { + "national" => 13, + }, + "value" => "Prefers not to say", + }] + end + + ANSWER_OPTIONS = { + "18" => { "value" => "United Kingdom" }, + "17" => { "value" => "Republic of Ireland" }, + "19" => { "value" => "European Economic Area (EEA), excluding ROI" }, + "12" => { "value" => "Other" }, + "13" => { "value" => "Buyer prefers not to say" }, + }.freeze +end diff --git a/app/models/form/sales/questions/nationality1.rb b/app/models/form/sales/questions/nationality1.rb index 7f85b438e..68c2ce5ea 100644 --- a/app/models/form/sales/questions/nationality1.rb +++ b/app/models/form/sales/questions/nationality1.rb @@ -7,16 +7,6 @@ class Form::Sales::Questions::Nationality1 < ::Form::Question @type = "radio" @hint_text = "Buyer 1 is the person in the household who does the most paid work. If it’s a joint purchase and the buyers do the same amount of paid work, buyer 1 is whoever is the oldest." @answer_options = ANSWER_OPTIONS - @conditional_for = { - "othernational" => [12], - } - @hidden_in_check_answers = { - "depends_on" => [ - { - "national" => 12, - }, - ], - } @check_answers_card_number = 1 @inferred_check_answers_value = [{ "condition" => { diff --git a/app/models/form/sales/questions/other_nationality1.rb b/app/models/form/sales/questions/other_nationality1.rb index 6225bf9a9..e69de29bb 100644 --- a/app/models/form/sales/questions/other_nationality1.rb +++ b/app/models/form/sales/questions/other_nationality1.rb @@ -1,11 +0,0 @@ -class Form::Sales::Questions::OtherNationality1 < ::Form::Question - def initialize(id, hsh, page) - super - @id = "othernational" - @check_answer_label = "Buyer 1’s nationality" - @header = "Nationality" - @type = "text" - @check_answers_card_number = 1 - @question_number = 24 - end -end diff --git a/app/models/form/sales/questions/previous_tenure_buyer2.rb b/app/models/form/sales/questions/previous_tenure_buyer2.rb new file mode 100644 index 000000000..49d495682 --- /dev/null +++ b/app/models/form/sales/questions/previous_tenure_buyer2.rb @@ -0,0 +1,23 @@ +class Form::Sales::Questions::PreviousTenureBuyer2 < ::Form::Question + def initialize(id, hsh, page) + super + @id = "prevtenbuy2" + @check_answer_label = "Buyer 2’s previous tenure" + @header = "What was buyer 2’s previous tenure?" + @type = "radio" + @hint_text = "" + @answer_options = ANSWER_OPTIONS + end + + ANSWER_OPTIONS = { + "1" => { "value" => "Local authority tenant" }, + "2" => { "value" => "Private registered provider or housing association tenant" }, + "3" => { "value" => "Private tenant" }, + "5" => { "value" => "Owner occupier" }, + "4" => { "value" => "Tied home or renting with job" }, + "6" => { "value" => "Living with family or friends" }, + "7" => { "value" => "Temporary accommodation" }, + "9" => { "value" => "Other" }, + "0" => { "value" => "Don't know" }, + }.freeze +end diff --git a/app/models/form/sales/subsections/household_characteristics.rb b/app/models/form/sales/subsections/household_characteristics.rb index 0277acffb..311162e03 100644 --- a/app/models/form/sales/subsections/household_characteristics.rb +++ b/app/models/form/sales/subsections/household_characteristics.rb @@ -32,7 +32,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection Form::Sales::Pages::RetirementValueCheck.new("age_2_buyer_retirement_value_check", nil, self, person_index: 2), Form::Sales::Pages::GenderIdentity2.new(nil, nil, self), Form::Sales::Pages::RetirementValueCheck.new("gender_2_buyer_retirement_value_check", nil, self, person_index: 2), - ethnic_pages_for_buyer_2, + buyer_2_ethnicity_nationality_pages, Form::Sales::Pages::Buyer2WorkingSituation.new(nil, nil, self), Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_retirement_value_check_joint_purchase", nil, self, person_index: 2), Form::Sales::Pages::Buyer2IncomeValueCheck.new("working_situation_buyer_2_income_value_check", nil, self), @@ -82,14 +82,17 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection ].flatten.compact end - def ethnic_pages_for_buyer_2 + def buyer_2_ethnicity_nationality_pages if form.start_date.year >= 2023 - [Form::Sales::Pages::Buyer2EthnicGroup.new(nil, nil, self), - Form::Sales::Pages::Buyer2EthnicBackgroundBlack.new(nil, nil, self), - Form::Sales::Pages::Buyer2EthnicBackgroundAsian.new(nil, nil, self), - Form::Sales::Pages::Buyer2EthnicBackgroundArab.new(nil, nil, self), - Form::Sales::Pages::Buyer2EthnicBackgroundMixed.new(nil, nil, self), - Form::Sales::Pages::Buyer2EthnicBackgroundWhite.new(nil, nil, self)] + [ + Form::Sales::Pages::Buyer2EthnicGroup.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundBlack.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundAsian.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundArab.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundMixed.new(nil, nil, self), + Form::Sales::Pages::Buyer2EthnicBackgroundWhite.new(nil, nil, self), + Form::Sales::Pages::Buyer2Nationality.new(nil, nil, self), + ] end end diff --git a/app/models/form/sales/subsections/household_situation.rb b/app/models/form/sales/subsections/household_situation.rb index a9d9306d6..225b1ae43 100644 --- a/app/models/form/sales/subsections/household_situation.rb +++ b/app/models/form/sales/subsections/household_situation.rb @@ -12,6 +12,16 @@ class Form::Sales::Subsections::HouseholdSituation < ::Form::Subsection Form::Sales::Pages::LastAccommodation.new(nil, nil, self), Form::Sales::Pages::LastAccommodationLa.new(nil, nil, self), Form::Sales::Pages::BuyersOrganisations.new(nil, nil, self), - ] + buyer_2_situation_pages, + ].flatten.compact + end + + def buyer_2_situation_pages + if form.start_date.year >= 2023 + [ + Form::Sales::Pages::Buyer2LivingIn.new(nil, nil, self), + Form::Sales::Pages::Buyer2PreviousHousingSituation.new(nil, nil, self), + ] + end end end diff --git a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb index bfaa622b2..68b9dd5c6 100644 --- a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb +++ b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb @@ -44,6 +44,8 @@ module Forms private def in_crossover_period? + return true if FeatureToggle.force_crossover? + FormHandler.instance.lettings_in_crossover_period? end end diff --git a/app/models/forms/bulk_upload_lettings/year.rb b/app/models/forms/bulk_upload_lettings/year.rb index 80324e53e..d958f27d4 100644 --- a/app/models/forms/bulk_upload_lettings/year.rb +++ b/app/models/forms/bulk_upload_lettings/year.rb @@ -34,7 +34,10 @@ module Forms private def possible_years - [FormHandler.instance.lettings_forms["current_lettings"].start_date.year, FormHandler.instance.lettings_forms["previous_lettings"].start_date.year] + [ + FormHandler.instance.lettings_forms["next_lettings"].start_date.year, + FormHandler.instance.lettings_forms["current_lettings"].start_date.year, + ] end end end diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 6491c100f..dad1ef705 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -195,6 +195,14 @@ class LettingsLog < Log renewal == 1 end + def starter_tenancy? + startertenancy == 1 + end + + def tenancy_type_fixed_term? + [4, 6].include? tenancy + end + def is_general_needs? # 1: General Needs needstype == 1 @@ -417,27 +425,13 @@ class LettingsLog < Log created_by&.is_dpo end - delegate :service_name, :sensitive, :registered_under_care_act, :primary_client_group, :has_other_client_group, :secondary_client_group, :owning_organisation, :managing_organisation, :support_type, :intended_stay, :created_at, prefix: "scheme", to: :scheme, allow_nil: true - delegate :scheme_type, to: :scheme, allow_nil: true - def scheme_code scheme&.id ? "S#{scheme.id}" : nil end - def scheme_owning_organisation_name - scheme_owning_organisation&.name - end - - delegate :postcode, :name, :units, :type_of_unit, :mobility_type, :startdate, prefix: "location", to: :location, allow_nil: true - delegate :location_admin_district, to: :location, allow_nil: true - - # This is not the location_code in the db, location.id is just called code in the UI - def location_code - location&.id - end - - def self.to_csv(user = nil) - Csv::LettingsLogCsvService.new(user).to_csv + def self.to_csv(user = nil, codes_only_export:) + export_type = codes_only_export ? "codes" : "labels" + Csv::LettingsLogCsvService.new(user, export_type:).to_csv end def beds_for_la_rent_range diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index 90f130c04..e15b9c610 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -151,6 +151,14 @@ class SalesLog < Log inc1mort == 1 end + def buyer_two_will_live_in_property? + buy2livein == 1 + end + + def buyer_two_not_already_living_in_property? + buy2living == 2 + end + def income2_used_for_mortgage? inc2mort == 1 end @@ -250,6 +258,10 @@ class SalesLog < Log jointpur == 2 end + def buyer_not_interviewed? + noint == 1 + end + def old_persons_shared_ownership? type == 24 end diff --git a/app/models/validations/sales/financial_validations.rb b/app/models/validations/sales/financial_validations.rb index 21f3743ca..255f12bf2 100644 --- a/app/models/validations/sales/financial_validations.rb +++ b/app/models/validations/sales/financial_validations.rb @@ -52,6 +52,21 @@ module Validations::Sales::FinancialValidations end end + def validate_percentage_bought_at_least_threshold(record) + return unless record.stairbought && record.type + + threshold = if [2, 16, 18, 24].include? record.type + 10 + else + 1 + end + + if threshold && record.stairbought < threshold + record.errors.add :stairbought, I18n.t("validations.financial.staircasing.percentage_bought_must_be_at_least_threshold", threshold:) + record.errors.add :type, I18n.t("validations.setup.type.percentage_bought_must_be_at_least_threshold", threshold:) + end + end + def validate_child_income(record) return unless record.income2 && record.ecstat2 diff --git a/app/services/bulk_upload/lettings/log_creator.rb b/app/services/bulk_upload/lettings/log_creator.rb index 625d53f43..089639d4f 100644 --- a/app/services/bulk_upload/lettings/log_creator.rb +++ b/app/services/bulk_upload/lettings/log_creator.rb @@ -26,7 +26,14 @@ class BulkUpload::Lettings::LogCreator private def csv_parser - @csv_parser ||= BulkUpload::Lettings::CsvParser.new(path:) + @csv_parser ||= case bulk_upload.year + when 2022 + BulkUpload::Lettings::Year2022::CsvParser.new(path:) + when 2023 + BulkUpload::Lettings::Year2023::CsvParser.new(path:) + else + raise "csv parser not found" + end end def row_offset diff --git a/app/services/bulk_upload/lettings/validator.rb b/app/services/bulk_upload/lettings/validator.rb index f5450039f..e60435f75 100644 --- a/app/services/bulk_upload/lettings/validator.rb +++ b/app/services/bulk_upload/lettings/validator.rb @@ -6,146 +6,10 @@ class BulkUpload::Lettings::Validator include ActiveModel::Validations - QUESTIONS = { - field_1: "What is the letting type?", - field_2: "This question has been removed", - field_3: "This question has been removed", - field_4: "Management group code", - field_5: "Scheme code", - field_6: "This question has been removed", - field_7: "What is the tenant code?", - field_8: "Is this a starter tenancy?", - field_9: "What is the tenancy type?", - field_10: "If 'Other', what is the tenancy type?", - field_11: "What is the length of the fixed-term tenancy to the nearest year?", - field_12: "Age of Person 1", - field_13: "Age of Person 2", - field_14: "Age of Person 3", - field_15: "Age of Person 4", - field_16: "Age of Person 5", - field_17: "Age of Person 6", - field_18: "Age of Person 7", - field_19: "Age of Person 8", - field_20: "Gender identity of Person 1", - field_21: "Gender identity of Person 2", - field_22: "Gender identity of Person 3", - field_23: "Gender identity of Person 4", - field_24: "Gender identity of Person 5", - field_25: "Gender identity of Person 6", - field_26: "Gender identity of Person 7", - field_27: "Gender identity of Person 8", - field_28: "Relationship to Person 1 for Person 2", - field_29: "Relationship to Person 1 for Person 3", - field_30: "Relationship to Person 1 for Person 4", - field_31: "Relationship to Person 1 for Person 5", - field_32: "Relationship to Person 1 for Person 6", - field_33: "Relationship to Person 1 for Person 7", - field_34: "Relationship to Person 1 for Person 8", - field_35: "Working situation of Person 1", - field_36: "Working situation of Person 2", - field_37: "Working situation of Person 3", - field_38: "Working situation of Person 4", - field_39: "Working situation of Person 5", - field_40: "Working situation of Person 6", - field_41: "Working situation of Person 7", - field_42: "Working situation of Person 8", - field_43: "What is the lead tenant's ethnic group?", - field_44: "What is the lead tenant's nationality?", - field_45: "Does anybody in the household have links to the UK armed forces?", - field_46: "Was the person seriously injured or ill as a result of serving in the UK armed forces?", - field_47: "Is anybody in the household pregnant?", - field_48: "Is the tenant likely to be receiving benefits related to housing?", - field_49: "How much of the household's income is from Universal Credit, state pensions or benefits?", - field_50: "How much income does the household have in total?", - field_51: "Do you know the household's income?", - field_52: "What is the tenant's main reason for the household leaving their last settled home?", - field_53: "If 'Other', what was the main reason for leaving their last settled home?", - field_54: "This question has been removed", - field_55: "Does anybody in the household have any disabled access needs?", - field_56: "Does anybody in the household have any disabled access needs?", - field_57: "Does anybody in the household have any disabled access needs?", - field_58: "Does anybody in the household have any disabled access needs?", - field_59: "Does anybody in the household have any disabled access needs?", - field_60: "Does anybody in the household have any disabled access needs?", - field_61: "Where was the household immediately before this letting?", - field_62: "What is the local authority of the household's last settled home?", - field_63: "Part 1 of postcode of last settled home", - field_64: "Part 2 of postcode of last settled home", - field_65: "Do you know the postcode of last settled home?", - field_66: "How long has the household continuously lived in the local authority area of the new letting?", - field_67: "How long has the household been on the waiting list for the new letting?", - field_68: "Was the tenant homeless directly before this tenancy?", - field_69: "Was the household given 'reasonable preference' by the local authority?", - field_70: "Reasonable preference. They were homeless or about to lose their home (within 56 days)", - field_71: "Reasonable preference. They were living in insanitary, overcrowded or unsatisfactory housing", - field_72: "Reasonable preference. They needed to move on medical and welfare grounds (including a disability)", - field_73: "Reasonable preference. They needed to move to avoid hardship to themselves or others", - field_74: "Reasonable preference. Don't know", - field_75: "Was the letting made under any of the following allocations systems?", - field_76: "Was the letting made under any of the following allocations systems?", - field_77: "Was the letting made under any of the following allocations systems?", - field_78: "What was the source of referral for this letting?", - field_79: "How often does the household pay rent and other charges?", - field_80: "What is the basic rent?", - field_81: "What is the service charge?", - field_82: "What is the personal service charge?", - field_83: "What is the support charge?", - field_84: "Total Charge", - field_85: "If this is a care home, how much does the household pay every [time period]?", - field_86: "Does the household pay rent or other charges for the accommodation?", - field_87: "After the household has received any housing-related benefits, will they still need to pay basic rent and other charges?", - field_88: "What do you expect the outstanding amount to be?", - field_89: "What is the void or renewal date?", - field_90: "What is the void or renewal date?", - field_91: "What is the void or renewal date?", - field_92: "What date were major repairs completed on?", - field_93: "What date were major repairs completed on?", - field_94: "What date were major repairs completed on?", - field_95: "This question has been removed", - field_96: "What date did the tenancy start?", - field_97: "What date did the tenancy start?", - field_98: "What date did the tenancy start?", - field_99: "Since becoming available, how many times has the property been previously offered?", - field_100: "What is the property reference?", - field_101: "How many bedrooms does the property have?", - field_102: "What type of unit is the property?", - field_103: "Which type of building is the property?", - field_104: "Is the property built or adapted to wheelchair-user standards?", - field_105: "What type was the property most recently let as?", - field_106: "What is the reason for the property being vacant?", - field_107: "What is the local authority of the property?", - field_108: "Part 1 of postcode of the property", - field_109: "Part 2 of postcode of the property", - field_110: "This question has been removed", - field_111: "Which organisation owns this property?", - field_112: "Username field", - field_113: "Which organisation manages this property?", - field_114: "Is the person still serving in the UK armed forces?", - field_115: "This question has been removed", - field_116: "How often does the household receive income?", - field_117: "Is this letting sheltered accommodation?", - field_118: "Does anybody in the household have a physical or mental health condition (or other illness) expected to last for 12 months or more?", - field_119: "Vision, for example blindness or partial sight", - field_120: "Hearing, for example deafness or partial hearing", - field_121: "Mobility, for example walking short distances or climbing stairs", - field_122: "Dexterity, for example lifting and carrying objects, using a keyboard", - field_123: "Learning or understanding or concentrating", - field_124: "Memory", - field_125: "Mental health", - field_126: "Stamina or breathing or fatigue", - field_127: "Socially or behaviourally, for example associated with autism spectral disorder (ASD) which includes Aspergers' or attention deficit hyperactivity disorder (ADHD)", - field_128: "Other", - field_129: "Is this letting a London Affordable Rent letting?", - field_130: "Which type of Intermediate Rent is this letting?", - field_131: "Which 'Other' type of Intermediate Rent is this letting?", - field_132: "Data Protection", - field_133: "Is this a joint tenancy?", - field_134: "Is this letting a renewal?", - }.freeze - attr_reader :bulk_upload, :path validate :validate_file_not_empty + validate :validate_min_columns validate :validate_max_columns def initialize(bulk_upload:, path:) @@ -160,14 +24,16 @@ class BulkUpload::Lettings::Validator row = index + row_offset + 1 row_parser.errors.each do |error| + col = csv_parser.column_for_field(error.attribute.to_s) + bulk_upload.bulk_upload_errors.create!( field: error.attribute, error: error.message, - tenant_code: row_parser.field_7, - property_ref: row_parser.field_100, + tenant_code: row_parser.tenant_code, + property_ref: row_parser.property_ref, row:, - cell: "#{cols[field_number_for_attribute(error.attribute) - col_offset + 1]}#{row}", - col: cols[field_number_for_attribute(error.attribute) - col_offset + 1], + cell: "#{col}#{row}", + col:, category: error.options[:category], ) end @@ -210,7 +76,14 @@ private end def csv_parser - @csv_parser ||= BulkUpload::Lettings::CsvParser.new(path:) + @csv_parser ||= case bulk_upload.year + when 2022 + BulkUpload::Lettings::Year2022::CsvParser.new(path:) + when 2023 + BulkUpload::Lettings::Year2023::CsvParser.new(path:) + else + raise "csv parser not found" + end end def row_offset @@ -257,12 +130,20 @@ private end end + def validate_min_columns + return if halt_validations? + + column_count = rows.map(&:size).min + + errors.add(:base, :under_min_column_count) if column_count < csv_parser.class::MIN_COLUMNS + end + def validate_max_columns return if halt_validations? - max_row_size = rows.map(&:size).max + column_count = rows.map(&:size).max - errors.add(:file, :max_row_size) if max_row_size > 136 + errors.add(:base, :over_max_column_count) if column_count > csv_parser.class::MAX_COLUMNS end def halt_validations! diff --git a/app/services/bulk_upload/lettings/csv_parser.rb b/app/services/bulk_upload/lettings/year2022/csv_parser.rb similarity index 75% rename from app/services/bulk_upload/lettings/csv_parser.rb rename to app/services/bulk_upload/lettings/year2022/csv_parser.rb index 6cead61ab..c0d8d8900 100644 --- a/app/services/bulk_upload/lettings/csv_parser.rb +++ b/app/services/bulk_upload/lettings/year2022/csv_parser.rb @@ -1,6 +1,9 @@ require "csv" -class BulkUpload::Lettings::CsvParser +class BulkUpload::Lettings::Year2022::CsvParser + MIN_COLUMNS = 134 + MAX_COLUMNS = 136 + attr_reader :path def initialize(path:) @@ -25,7 +28,7 @@ class BulkUpload::Lettings::CsvParser headers = ("field_1".."field_134").to_a hash = Hash[headers.zip(stripped_row)] - BulkUpload::Lettings::RowParser.new(hash) + BulkUpload::Lettings::Year2022::RowParser.new(hash) end end @@ -37,8 +40,16 @@ class BulkUpload::Lettings::CsvParser @rows ||= CSV.parse(normalised_string, row_sep:) end + def column_for_field(field) + cols[headers.find_index(field) + col_offset] + end + private + def headers + @headers ||= ("field_1".."field_134").to_a + end + def with_headers? rows[0][0]&.match?(/\D+/) end diff --git a/app/services/bulk_upload/lettings/row_parser.rb b/app/services/bulk_upload/lettings/year2022/row_parser.rb similarity index 78% rename from app/services/bulk_upload/lettings/row_parser.rb rename to app/services/bulk_upload/lettings/year2022/row_parser.rb index ea1ca963a..560ed82b5 100644 --- a/app/services/bulk_upload/lettings/row_parser.rb +++ b/app/services/bulk_upload/lettings/year2022/row_parser.rb @@ -1,7 +1,144 @@ -class BulkUpload::Lettings::RowParser +class BulkUpload::Lettings::Year2022::RowParser include ActiveModel::Model include ActiveModel::Attributes + QUESTIONS = { + field_1: "What is the letting type?", + field_2: "This question has been removed", + field_3: "This question has been removed", + field_4: "Management group code", + field_5: "Scheme code", + field_6: "This question has been removed", + field_7: "What is the tenant code?", + field_8: "Is this a starter tenancy?", + field_9: "What is the tenancy type?", + field_10: "If 'Other', what is the tenancy type?", + field_11: "What is the length of the fixed-term tenancy to the nearest year?", + field_12: "Age of Person 1", + field_13: "Age of Person 2", + field_14: "Age of Person 3", + field_15: "Age of Person 4", + field_16: "Age of Person 5", + field_17: "Age of Person 6", + field_18: "Age of Person 7", + field_19: "Age of Person 8", + field_20: "Gender identity of Person 1", + field_21: "Gender identity of Person 2", + field_22: "Gender identity of Person 3", + field_23: "Gender identity of Person 4", + field_24: "Gender identity of Person 5", + field_25: "Gender identity of Person 6", + field_26: "Gender identity of Person 7", + field_27: "Gender identity of Person 8", + field_28: "Relationship to Person 1 for Person 2", + field_29: "Relationship to Person 1 for Person 3", + field_30: "Relationship to Person 1 for Person 4", + field_31: "Relationship to Person 1 for Person 5", + field_32: "Relationship to Person 1 for Person 6", + field_33: "Relationship to Person 1 for Person 7", + field_34: "Relationship to Person 1 for Person 8", + field_35: "Working situation of Person 1", + field_36: "Working situation of Person 2", + field_37: "Working situation of Person 3", + field_38: "Working situation of Person 4", + field_39: "Working situation of Person 5", + field_40: "Working situation of Person 6", + field_41: "Working situation of Person 7", + field_42: "Working situation of Person 8", + field_43: "What is the lead tenant's ethnic group?", + field_44: "What is the lead tenant's nationality?", + field_45: "Does anybody in the household have links to the UK armed forces?", + field_46: "Was the person seriously injured or ill as a result of serving in the UK armed forces?", + field_47: "Is anybody in the household pregnant?", + field_48: "Is the tenant likely to be receiving benefits related to housing?", + field_49: "How much of the household's income is from Universal Credit, state pensions or benefits?", + field_50: "How much income does the household have in total?", + field_51: "Do you know the household's income?", + field_52: "What is the tenant's main reason for the household leaving their last settled home?", + field_53: "If 'Other', what was the main reason for leaving their last settled home?", + field_54: "This question has been removed", + field_55: "Does anybody in the household have any disabled access needs?", + field_56: "Does anybody in the household have any disabled access needs?", + field_57: "Does anybody in the household have any disabled access needs?", + field_58: "Does anybody in the household have any disabled access needs?", + field_59: "Does anybody in the household have any disabled access needs?", + field_60: "Does anybody in the household have any disabled access needs?", + field_61: "Where was the household immediately before this letting?", + field_62: "What is the local authority of the household's last settled home?", + field_63: "Part 1 of postcode of last settled home", + field_64: "Part 2 of postcode of last settled home", + field_65: "Do you know the postcode of last settled home?", + field_66: "How long has the household continuously lived in the local authority area of the new letting?", + field_67: "How long has the household been on the waiting list for the new letting?", + field_68: "Was the tenant homeless directly before this tenancy?", + field_69: "Was the household given 'reasonable preference' by the local authority?", + field_70: "Reasonable preference. They were homeless or about to lose their home (within 56 days)", + field_71: "Reasonable preference. They were living in insanitary, overcrowded or unsatisfactory housing", + field_72: "Reasonable preference. They needed to move on medical and welfare grounds (including a disability)", + field_73: "Reasonable preference. They needed to move to avoid hardship to themselves or others", + field_74: "Reasonable preference. Don't know", + field_75: "Was the letting made under any of the following allocations systems?", + field_76: "Was the letting made under any of the following allocations systems?", + field_77: "Was the letting made under any of the following allocations systems?", + field_78: "What was the source of referral for this letting?", + field_79: "How often does the household pay rent and other charges?", + field_80: "What is the basic rent?", + field_81: "What is the service charge?", + field_82: "What is the personal service charge?", + field_83: "What is the support charge?", + field_84: "Total Charge", + field_85: "If this is a care home, how much does the household pay every [time period]?", + field_86: "Does the household pay rent or other charges for the accommodation?", + field_87: "After the household has received any housing-related benefits, will they still need to pay basic rent and other charges?", + field_88: "What do you expect the outstanding amount to be?", + field_89: "What is the void or renewal date?", + field_90: "What is the void or renewal date?", + field_91: "What is the void or renewal date?", + field_92: "What date were major repairs completed on?", + field_93: "What date were major repairs completed on?", + field_94: "What date were major repairs completed on?", + field_95: "This question has been removed", + field_96: "What date did the tenancy start?", + field_97: "What date did the tenancy start?", + field_98: "What date did the tenancy start?", + field_99: "Since becoming available, how many times has the property been previously offered?", + field_100: "What is the property reference?", + field_101: "How many bedrooms does the property have?", + field_102: "What type of unit is the property?", + field_103: "Which type of building is the property?", + field_104: "Is the property built or adapted to wheelchair-user standards?", + field_105: "What type was the property most recently let as?", + field_106: "What is the reason for the property being vacant?", + field_107: "What is the local authority of the property?", + field_108: "Part 1 of postcode of the property", + field_109: "Part 2 of postcode of the property", + field_110: "This question has been removed", + field_111: "Which organisation owns this property?", + field_112: "Username field", + field_113: "Which organisation manages this property?", + field_114: "Is the person still serving in the UK armed forces?", + field_115: "This question has been removed", + field_116: "How often does the household receive income?", + field_117: "Is this letting sheltered accommodation?", + field_118: "Does anybody in the household have a physical or mental health condition (or other illness) expected to last for 12 months or more?", + field_119: "Vision, for example blindness or partial sight", + field_120: "Hearing, for example deafness or partial hearing", + field_121: "Mobility, for example walking short distances or climbing stairs", + field_122: "Dexterity, for example lifting and carrying objects, using a keyboard", + field_123: "Learning or understanding or concentrating", + field_124: "Memory", + field_125: "Mental health", + field_126: "Stamina or breathing or fatigue", + field_127: "Socially or behaviourally, for example associated with autism spectral disorder (ASD) which includes Aspergers' or attention deficit hyperactivity disorder (ADHD)", + field_128: "Other", + field_129: "Is this letting a London Affordable Rent letting?", + field_130: "Which type of Intermediate Rent is this letting?", + field_131: "Which 'Other' type of Intermediate Rent is this letting?", + field_132: "Data Protection", + field_133: "Is this a joint tenancy?", + field_134: "Is this letting a renewal?", + }.freeze + attribute :bulk_upload attribute :block_log_creation, :boolean, default: -> { false } @@ -188,6 +325,10 @@ class BulkUpload::Lettings::RowParser validate :validate_location_exists validate :validate_location_data_given + def self.question_for_field(field) + QUESTIONS[field] + end + def valid? errors.clear @@ -211,7 +352,12 @@ class BulkUpload::Lettings::RowParser end def blank_row? - attribute_set.to_hash.reject { |k, _| %w[bulk_upload block_log_creation].include?(k) }.values.compact.empty? + attribute_set + .to_hash + .reject { |k, _| %w[bulk_upload block_log_creation].include?(k) } + .values + .compact + .empty? end def log @@ -226,6 +372,14 @@ class BulkUpload::Lettings::RowParser block_log_creation end + def tenant_code + field_7 + end + + def property_ref + field_100 + end + private def validate_location_related diff --git a/app/services/bulk_upload/lettings/year2023/csv_parser.rb b/app/services/bulk_upload/lettings/year2023/csv_parser.rb new file mode 100644 index 000000000..6f661922c --- /dev/null +++ b/app/services/bulk_upload/lettings/year2023/csv_parser.rb @@ -0,0 +1,81 @@ +require "csv" + +class BulkUpload::Lettings::Year2023::CsvParser + MIN_COLUMNS = 141 + MAX_COLUMNS = 143 + + attr_reader :path + + def initialize(path:) + @path = path + end + + def row_offset + if with_headers? + rows.find_index { |row| row[0].match(/field number/i) } + 1 + else + 0 + end + end + + def col_offset + with_headers? ? 1 : 0 + end + + def cols + @cols ||= ("A".."EL").to_a + end + + def row_parsers + @row_parsers ||= body_rows.map do |row| + stripped_row = row[col_offset..] + hash = Hash[field_numbers.zip(stripped_row)] + + BulkUpload::Lettings::Year2023::RowParser.new(hash) + end + end + + def body_rows + rows[row_offset..] + end + + def rows + @rows ||= CSV.parse(normalised_string, row_sep:) + end + + def column_for_field(field) + cols[field_numbers.find_index(field) + col_offset] + end + +private + + def default_field_numbers + [5, nil, nil, 15, 16, nil, 13, 40, 41, 42, 43, 46, 52, 56, 60, 64, 68, 72, 76, 47, 53, 57, 61, 65, 69, 73, 77, 51, 55, 59, 63, 67, 71, 75, 50, 54, 58, 62, 66, 70, 74, 78, 48, 49, 79, 81, 82, 123, 124, 122, 120, 102, 103, nil, 83, 84, 85, 86, 87, 88, 104, 109, 107, 108, 106, 100, 101, 105, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 126, 128, 129, 130, 131, 132, 127, 125, 133, 134, 33, 34, 35, 36, 37, 38, nil, 7, 8, 9, 28, 14, 32, 29, 30, 31, 26, 27, 25, 23, 24, nil, 1, 3, 2, 80, nil, 121, 44, 89, 98, 92, 95, 90, 91, 93, 94, 97, 96, 99, 10, 11, 12, 45, 39, 6, 4, 17, 18, 19, 20, 21, 22].map { |h| h.present? ? "field_#{h}" : "field_blank" } + end + + def field_numbers + @field_numbers ||= if with_headers? + rows[row_offset - 1][col_offset..].map { |h| h.present? ? "field_#{h}" : "field_blank" } + else + default_field_numbers + end + end + + def with_headers? + rows.map { |r| r[0] }.any? { |cell| cell&.match?(/field number/i) } + end + + def row_sep + "\n" + end + + def normalised_string + return @normalised_string if @normalised_string + + @normalised_string = File.read(path, encoding: "bom|utf-8") + @normalised_string.gsub!("\r\n", "\n") + @normalised_string.scrub!("") + + @normalised_string + end +end diff --git a/app/services/bulk_upload/lettings/year2023/row_parser.rb b/app/services/bulk_upload/lettings/year2023/row_parser.rb new file mode 100644 index 000000000..724b1c2ef --- /dev/null +++ b/app/services/bulk_upload/lettings/year2023/row_parser.rb @@ -0,0 +1,1241 @@ +class BulkUpload::Lettings::Year2023::RowParser + include ActiveModel::Model + include ActiveModel::Attributes + + QUESTIONS = { + field_1: "Which organisation owns this property?", + field_2: "Which organisation manages this letting?", + field_3: "What is the CORE username of the account this letting log should be assigned to?", + field_4: "What is the needs type?", + field_5: "What is the letting type?", + field_6: "Is this letting a renewal?", + field_7: "What is the tenancy start date?", + field_8: "What is the tenancy start date?", + field_9: "What is the tenancy start date?", + field_10: "Is this a London Affordable Rent letting?", + field_11: "Which type of Intermediate Rent is this letting?", + field_12: "Which 'Other' type of Intermediate Rent is this letting?", + field_13: "What is the tenant code?", + field_14: "What is the property reference?", + field_15: "What management group does this letting belong to?", + field_16: "What scheme does this letting belong to?", + field_17: "Which location is this letting for?", + field_18: "If known, provide this property’s UPRN", + field_19: "Address line 1", + field_20: "Address line 2", + field_21: "Town or city", + field_22: "County", + field_23: "Part 1 of the property's postcode", + field_24: "Part 2 of the property's postcode", + field_25: "What is the property's local authority?", + field_26: "What type was the property most recently let as?", + field_27: "What is the reason for the property being vacant?", + field_28: "How many times was the property offered between becoming vacant and this letting?", + field_29: "What type of unit is the property?", + field_30: "Which type of building is the property?", + field_31: "Is the property built or adapted to wheelchair-user standards?", + field_32: "How many bedrooms does the property have?", + field_33: "What is the void date?", + field_34: "What is the void date?", + field_35: "What is the void date?", + field_36: "What date were any major repairs completed on?", + field_37: "What date were any major repairs completed on?", + field_38: "What date were any major repairs completed on?", + field_39: "Is this a joint tenancy?", + field_40: "Is this a starter tenancy?", + field_41: "What is the type of tenancy?", + field_42: "If 'Other', what is the type of tenancy?", + field_43: "What is the length of the fixed-term tenancy to the nearest year?", + field_44: "Is this letting sheltered accommodation?", + field_45: "Has tenant seen the DLUHC privacy notice?", + field_46: "What is the lead tenant's age?", + field_47: "Which of these best describes the lead tenant's gender identity?", + field_48: "Which of these best describes the lead tenant's ethnic background?", + field_49: "What is the lead tenant's nationality?", + field_50: "Which of these best describes the lead tenant's working situation?", + field_51: "What is person 2's relationship to the lead tenant?", + field_52: "What is person 2's age?", + field_53: "Which of these best describes person 2's gender identity?", + field_54: "Which of these best describes person 2's working situation?", + field_55: "What is person 3's relationship to the lead tenant?", + field_56: "What is person 3's age?", + field_57: "Which of these best describes person 3's gender identity?", + field_58: "Which of these best describes person 3's working situation?", + field_59: "What is person 4's relationship to the lead tenant?", + field_60: "What is person 4's age?", + field_61: "Which of these best describes person 4's gender identity?", + field_62: "Which of these best describes person 4's working situation?", + field_63: "What is person 5's relationship to the lead tenant?", + field_64: "What is person 5's age?", + field_65: "Which of these best describes person 5's gender identity?", + field_66: "Which of these best describes person 5's working situation?", + field_67: "What is person 6's relationship to the lead tenant?", + field_68: "What is person 6's age?", + field_69: "Which of these best describes person 6's gender identity?", + field_70: "Which of these best describes person 6's working situation?", + field_71: "What is person 7's relationship to the lead tenant?", + field_72: "What is person 7's age?", + field_73: "Which of these best describes person 7's gender identity?", + field_74: "Which of these best describes person 7's working situation?", + field_75: "What is person 8's relationship to the lead tenant?", + field_76: "What is person 8's age?", + field_77: "Which of these best describes person 8's gender identity?", + field_78: "Which of these best describes person 8's working situation?", + field_79: "Does anybody in the household have links to the UK armed forces?", + field_80: "Is this person still serving in the UK armed forces?", + field_81: "Was this person seriously injured or ill as a result of serving in the UK armed forces?", + field_82: "Is anybody in the household pregnant?", + field_83: "Does anybody in the household have any disabled access needs?", + field_84: "Does anybody in the household have any disabled access needs?", + field_85: "Does anybody in the household have any disabled access needs?", + field_86: "Does anybody in the household have any disabled access needs?", + field_87: "Does anybody in the household have any disabled access needs?", + field_88: "Does anybody in the household have any disabled access needs?", + field_89: "Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more?", + field_90: "Does this person's condition affect their dexterity?", + field_91: "Does this person's condition affect their learning or understanding or concentrating?", + field_92: "Does this person's condition affect their hearing?", + field_93: "Does this person's condition affect their memory?", + field_94: "Does this person's condition affect their mental health?", + field_95: "Does this person's condition affect their mobility?", + field_96: "Does this person's condition affect them socially or behaviourally?", + field_97: "Does this person's condition affect their stamina or breathing or fatigue?", + field_98: "Does this person's condition affect their vision?", + field_99: "Does this person's condition affect them in another way?", + field_100: "How long has the household continuously lived in the local authority area of the new letting?", + field_101: "How long has the household been on the local authority waiting list for the new letting?", + field_102: "What is the tenant’s main reason for the household leaving their last settled home?", + field_103: "If 'Other', what was the main reason for leaving their last settled home?", + field_104: "Where was the household immediately before this letting?", + field_105: "Did the household experience homelessness immediately before this letting?", + field_106: "Do you know the postcode of the household's last settled home?", + field_107: "What is the postcode of the household's last settled home?", + field_108: "What is the postcode of the household's last settled home?", + field_109: "What is the local authority of the household's last settled home?", + field_110: "Was the household given 'reasonable preference' by the local authority?", + field_111: "Reasonable preference reason They were homeless or about to lose their home (within 56 days)", + field_112: "Reasonable preference reason They were living in insanitary, overcrowded or unsatisfactory housing", + field_113: "Reasonable preference reason They needed to move on medical and welfare reasons (including disability)", + field_114: "Reasonable preference reason They needed to move to avoid hardship to themselves or others", + field_115: "Reasonable preference reason Don't know", + field_116: "How was this letting allocated?", + field_117: "How was this letting allocated?", + field_118: "How was this letting allocated?", + field_119: "What was the source of referral for this letting?", + field_120: "Do you know the household's combined total income after tax?", + field_121: "How often does the household receive income?", + field_122: "How much income does the household have in total?", + field_123: "Is the tenant likely to be receiving any of these housing-related benefits?", + field_124: "How much of the household's income is from Universal Credit, state pensions or benefits?", + field_125: "Does the household pay rent or other charges for the accommodation?", + field_126: "How often does the household pay rent and other charges?", + field_127: "If this is a care home, how much does the household pay every [time period]?", + field_128: "What is the basic rent?", + field_129: "What is the service charge?", + field_130: "What is the personal service charge?", + field_131: "What is the support charge?", + field_132: "Total charge", + field_133: "After the household has received any housing-related benefits, will they still need to pay for rent and charges?", + field_134: "What do you expect the outstanding amount to be?", + }.freeze + + attribute :bulk_upload + attribute :block_log_creation, :boolean, default: -> { false } + + attribute :field_blank + + attribute :field_1, :string + attribute :field_2, :string + attribute :field_3, :string + attribute :field_4, :integer + attribute :field_5, :integer + attribute :field_6, :integer + attribute :field_7, :integer + attribute :field_8, :integer + attribute :field_9, :integer + attribute :field_10, :integer + attribute :field_11, :integer + attribute :field_12, :string + attribute :field_13, :string + attribute :field_14, :string + attribute :field_15, :string + attribute :field_16, :string + attribute :field_17, :string + attribute :field_18, :string + attribute :field_19, :string + attribute :field_20, :string + attribute :field_21, :string + attribute :field_22, :string + attribute :field_23, :string + attribute :field_24, :string + attribute :field_25, :string + attribute :field_26, :integer + attribute :field_27, :integer + attribute :field_28, :integer + attribute :field_29, :integer + attribute :field_30, :integer + attribute :field_31, :integer + attribute :field_32, :integer + attribute :field_33, :integer + attribute :field_34, :integer + attribute :field_35, :integer + attribute :field_36, :integer + attribute :field_37, :integer + attribute :field_38, :integer + attribute :field_39, :integer + attribute :field_40, :integer + attribute :field_41, :integer + attribute :field_42, :string + attribute :field_43, :integer + attribute :field_44, :integer + attribute :field_45, :integer + attribute :field_46, :string + attribute :field_47, :string + attribute :field_48, :integer + attribute :field_49, :integer + attribute :field_50, :integer + attribute :field_51, :string + attribute :field_52, :string + attribute :field_53, :string + attribute :field_54, :integer + attribute :field_55, :string + attribute :field_56, :string + attribute :field_57, :string + attribute :field_58, :integer + attribute :field_59, :string + attribute :field_60, :string + attribute :field_61, :string + attribute :field_62, :integer + attribute :field_63, :string + attribute :field_64, :string + attribute :field_65, :string + attribute :field_66, :integer + attribute :field_67, :string + attribute :field_68, :string + attribute :field_69, :string + attribute :field_70, :integer + attribute :field_71, :string + attribute :field_72, :string + attribute :field_73, :string + attribute :field_74, :integer + attribute :field_75, :string + attribute :field_76, :string + attribute :field_77, :string + attribute :field_78, :integer + attribute :field_79, :integer + attribute :field_80, :integer + attribute :field_81, :integer + attribute :field_82, :integer + attribute :field_83, :integer + attribute :field_84, :integer + attribute :field_85, :integer + attribute :field_86, :integer + attribute :field_87, :integer + attribute :field_88, :integer + attribute :field_89, :integer + attribute :field_90, :integer + attribute :field_91, :integer + attribute :field_92, :integer + attribute :field_93, :integer + attribute :field_94, :integer + attribute :field_95, :integer + attribute :field_96, :integer + attribute :field_97, :integer + attribute :field_98, :integer + attribute :field_99, :integer + attribute :field_100, :integer + attribute :field_101, :integer + attribute :field_102, :integer + attribute :field_103, :string + attribute :field_104, :integer + attribute :field_105, :integer + attribute :field_106, :integer + attribute :field_107, :string + attribute :field_108, :string + attribute :field_109, :string + attribute :field_110, :integer + attribute :field_111, :integer + attribute :field_112, :integer + attribute :field_113, :integer + attribute :field_114, :integer + attribute :field_115, :integer + attribute :field_116, :integer + attribute :field_117, :integer + attribute :field_118, :integer + attribute :field_119, :integer + attribute :field_120, :integer + attribute :field_121, :integer + attribute :field_122, :decimal + attribute :field_123, :integer + attribute :field_124, :integer + attribute :field_125, :integer + attribute :field_126, :integer + attribute :field_127, :decimal + attribute :field_128, :decimal + attribute :field_129, :decimal + attribute :field_130, :decimal + attribute :field_131, :decimal + attribute :field_132, :decimal + attribute :field_133, :integer + attribute :field_134, :decimal + + validates :field_5, 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_16, presence: { if: proc { [2, 4, 6, 8, 10, 12].include?(field_5) } } + + validates :field_46, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 1 must be a number or the letter R" } + validates :field_52, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 2 must be a number or the letter R" } + validates :field_56, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 3 must be a number or the letter R" } + validates :field_60, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 4 must be a number or the letter R" } + validates :field_64, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 5 must be a number or the letter R" } + validates :field_68, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 6 must be a number or the letter R" } + validates :field_72, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 7 must be a number or the letter R" } + validates :field_76, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 8 must be a number or the letter R" } + + validates :field_7, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (day)") } + validates :field_8, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (month)") } + validates :field_9, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (year)") } + + validates :field_9, format: { with: /\A\d{2}\z/, message: I18n.t("validations.setup.startdate.year_not_two_digits") } + + validate :validate_needs_type_present + validate :validate_data_types + validate :validate_nulls + validate :validate_relevant_collection_window + validate :validate_la_with_local_housing_referral + validate :validate_cannot_be_la_referral_if_general_needs_and_la + validate :validate_leaving_reason_for_renewal + validate :validate_lettings_type_matches_bulk_upload + validate :validate_only_one_housing_needs_type + validate :validate_no_disabled_needs_conjunction + validate :validate_dont_know_disabled_needs_conjunction + validate :validate_no_and_dont_know_disabled_needs_conjunction + + validate :validate_owning_org_permitted + validate :validate_owning_org_owns_stock + validate :validate_owning_org_exists + validate :validate_owning_org_data_given + + validate :validate_managing_org_related + validate :validate_managing_org_exists + validate :validate_managing_org_data_given + + validate :validate_scheme_related + validate :validate_scheme_exists + validate :validate_scheme_data_given + + validate :validate_location_related + validate :validate_location_exists + validate :validate_location_data_given + + def self.question_for_field(field) + QUESTIONS[field] + end + + def valid? + errors.clear + + return true if blank_row? + + super + + log.valid? + + log.errors.each do |error| + fields = field_mapping_for_errors[error.attribute] || [] + + fields.each do |field| + unless errors.include?(field) + errors.add(field, error.type) + end + end + end + + errors.blank? + end + + def blank_row? + attribute_set + .to_hash + .reject { |k, _| %w[bulk_upload block_log_creation field_blank].include?(k) } + .values + .compact + .empty? + end + + def log + @log ||= LettingsLog.new(attributes_for_log) + end + + def block_log_creation! + self.block_log_creation = true + end + + def block_log_creation? + block_log_creation + end + + def tenant_code + field_13 + end + + def property_ref + field_14 + end + +private + + def validate_needs_type_present + if field_4.blank? + errors.add(:field_4, I18n.t("validations.not_answered", question: "needs type")) + end + end + + def start_date + return if field_7.blank? || field_8.blank? || field_9.blank? + + Date.parse("20#{field_9.to_s.rjust(2, '0')}-#{field_8}-#{field_7}") + rescue StandardError + nil + end + + def validate_no_and_dont_know_disabled_needs_conjunction + if field_87 == 1 && field_88 == 1 + errors.add(:field_87, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction")) + errors.add(:field_88, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction")) + end + end + + def validate_dont_know_disabled_needs_conjunction + if field_88 == 1 && [field_83, field_84, field_85, field_86].compact.count.positive? + errors.add(:field_88, I18n.t("validations.household.housingneeds.dont_know_disabled_needs_conjunction")) + end + end + + def validate_no_disabled_needs_conjunction + if field_87 == 1 && [field_83, field_84, field_85, field_86].compact.count.positive? + errors.add(:field_87, I18n.t("validations.household.housingneeds.no_disabled_needs_conjunction")) + end + end + + def validate_only_one_housing_needs_type + if [field_83, field_84, field_85].compact.count.positive? + errors.add(:field_83, I18n.t("validations.household.housingneeds_type.only_one_option_permitted")) + errors.add(:field_84, I18n.t("validations.household.housingneeds_type.only_one_option_permitted")) + errors.add(:field_85, I18n.t("validations.household.housingneeds_type.only_one_option_permitted")) + end + end + + def validate_lettings_type_matches_bulk_upload + if [1, 3, 5, 7, 9, 11].include?(field_5) && !general_needs? + errors.add(:field_5, I18n.t("validations.setup.lettype.supported_housing_mismatch")) + end + + if [2, 4, 6, 8, 10, 12].include?(field_5) && !supported_housing? + errors.add(:field_5, I18n.t("validations.setup.lettype.general_needs_mismatch")) + end + end + + def validate_leaving_reason_for_renewal + if field_6 == 1 && ![40, 42].include?(field_102) + errors.add(:field_102, I18n.t("validations.household.reason.renewal_reason_needed")) + end + end + + def general_needs? + field_4 == 1 + end + + def supported_housing? + field_4 == 2 + end + + def validate_cannot_be_la_referral_if_general_needs_and_la + if field_119 == 4 && general_needs? && owning_organisation && owning_organisation.la? + errors.add :field_119, I18n.t("validations.household.referral.la_general_needs.prp_referred_by_la") + end + end + + def validate_la_with_local_housing_referral + if field_119 == 3 && owning_organisation && owning_organisation.la? + errors.add(:field_119, I18n.t("validations.household.referral.nominated_by_local_ha_but_la")) + end + end + + def validate_relevant_collection_window + return if start_date.blank? || bulk_upload.form.blank? + + unless bulk_upload.form.valid_start_date_for_form?(start_date) + errors.add(:field_7, I18n.t("validations.date.outside_collection_window")) + errors.add(:field_8, I18n.t("validations.date.outside_collection_window")) + errors.add(:field_9, I18n.t("validations.date.outside_collection_window")) + end + end + + def validate_data_types + unless attribute_set["field_5"].value_before_type_cast&.match?(/\A\d+\z/) + errors.add(:field_5, I18n.t("validations.invalid_number", question: "letting type")) + end + end + + def validate_nulls + field_mapping_for_errors.each do |error_key, fields| + question_id = error_key.to_s + question = questions.find { |q| q.id == question_id } + + next unless question + next if log.optional_fields.include?(question.id) + next if question.completed?(log) + + if setup_question?(question) + 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 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 + + def validate_location_related + return if scheme.blank? || location.blank? + + unless location.scheme == scheme + block_log_creation! + errors.add(:field_17, "Scheme code must relate to a location that is owned by owning organisation or managing organisation") + end + end + + def validate_location_exists + if scheme && field_17.present? && location.nil? + errors.add(:field_17, "Location could be found with provided scheme code") + end + end + + def validate_location_data_given + if supported_housing? && field_17.blank? + errors.add(:field_17, "The scheme code must be present", category: "setup") + end + end + + def validate_scheme_related + return unless field_16.present? && scheme.present? + + owned_by_owning_org = owning_organisation && scheme.owning_organisation == owning_organisation + owned_by_managing_org = managing_organisation && scheme.owning_organisation == managing_organisation + + unless owned_by_owning_org || owned_by_managing_org + block_log_creation! + errors.add(:field_16, "This management group code does not belong to your organisation, or any of your stock owners / managing agents") + end + end + + def validate_scheme_exists + if field_16.present? && scheme.nil? + errors.add(:field_16, "The management group code is not correct") + end + end + + def validate_scheme_data_given + if supported_housing? && field_16.blank? + errors.add(:field_16, "The management group code is not correct", category: "setup") + end + end + + def validate_managing_org_related + if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation) + block_log_creation! + errors.add(:field_2, "This managing organisation does not have a relationship with the owning organisation") + end + end + + def validate_managing_org_exists + if managing_organisation.nil? + errors.delete(:field_2) + errors.add(:field_2, "The managing organisation code is incorrect") + end + end + + def validate_managing_org_data_given + if field_2.blank? + errors.add(:field_2, "The managing organisation code is incorrect", category: :setup) + end + end + + def validate_owning_org_owns_stock + if owning_organisation && !owning_organisation.holds_own_stock? + block_log_creation! + errors.delete(:field_1) + errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock") + end + end + + def validate_owning_org_exists + if owning_organisation.nil? + errors.delete(:field_1) + errors.add(:field_1, "The owning organisation code is incorrect") + end + end + + def validate_owning_org_data_given + if field_1.blank? + errors.add(:field_1, "The owning organisation code is incorrect", category: :setup) + end + end + + def validate_owning_org_permitted + if owning_organisation && !bulk_upload.user.organisation.affiliated_stock_owners.include?(owning_organisation) + block_log_creation! + errors.delete(:field_1) + errors.add(:field_1, "You do not have permission to add logs for this owning organisation") + end + end + + def setup_question?(question) + log.form.setup_sections[0].subsections[0].questions.include?(question) + end + + def field_mapping_for_errors + { + lettype: [:field_5], + tenancycode: [:field_13], + postcode_known: %i[field_25 field_23 field_24], + postcode_full: %i[field_25 field_23 field_24], + la: %i[field_25], + owning_organisation: [:field_1], + managing_organisation: [:field_2], + owning_organisation_id: [:field_1], + managing_organisation_id: [:field_2], + renewal: [:field_6], + scheme: %i[field_16], + location: %i[field_17], + created_by: [], + needstype: [:field_4], + rent_type: %i[field_5 field_10 field_11], + startdate: %i[field_7 field_8 field_9], + unittype_gn: %i[field_29], + builtype: %i[field_30], + wchair: %i[field_31], + beds: %i[field_32], + joint: %i[field_39], + startertenancy: %i[field_40], + tenancy: %i[field_41], + tenancyother: %i[field_42], + tenancylength: %i[field_43], + declaration: %i[field_45], + + age1_known: %i[field_46], + age1: %i[field_46], + age2_known: %i[field_52], + age2: %i[field_52], + age3_known: %i[field_56], + age3: %i[field_56], + age4_known: %i[field_60], + age4: %i[field_60], + age5_known: %i[field_64], + age5: %i[field_64], + age6_known: %i[field_68], + age6: %i[field_68], + age7_known: %i[field_72], + age7: %i[field_72], + age8_known: %i[field_76], + age8: %i[field_76], + + sex1: %i[field_47], + sex2: %i[field_53], + sex3: %i[field_57], + sex4: %i[field_61], + sex5: %i[field_65], + sex6: %i[field_69], + sex7: %i[field_73], + sex8: %i[field_77], + + ethnic_group: %i[field_48], + ethnic: %i[field_48], + national: %i[field_49], + + relat2: %i[field_51], + relat3: %i[field_55], + relat4: %i[field_59], + relat5: %i[field_63], + relat6: %i[field_67], + relat7: %i[field_71], + relat8: %i[field_75], + + ecstat1: %i[field_50], + ecstat2: %i[field_54], + ecstat3: %i[field_58], + ecstat4: %i[field_62], + ecstat5: %i[field_66], + ecstat6: %i[field_70], + ecstat7: %i[field_74], + ecstat8: %i[field_78], + + armedforces: %i[field_79], + leftreg: %i[field_80], + reservist: %i[field_81], + preg_occ: %i[field_82], + housingneeds: %i[field_82], + + illness: %i[field_89], + + layear: %i[field_100], + waityear: %i[field_101], + reason: %i[field_102], + reasonother: %i[field_103], + prevten: %i[field_104], + homeless: %i[field_105], + + prevloc: %i[field_109], + previous_la_known: %i[field_109], + ppcodenk: %i[field_106], + ppostcode_full: %i[field_107 field_108], + + reasonpref: %i[field_110], + rp_homeless: %i[field_111], + rp_insan_unsat: %i[field_112], + rp_medwel: %i[field_113], + rp_hardship: %i[field_114], + rp_dontknow: %i[field_115], + + cbl: %i[field_116], + chr: %i[field_118], + cap: %i[field_117], + + referral: %i[field_119], + + net_income_known: %i[field_120], + earnings: %i[field_122], + incfreq: %i[field_121], + hb: %i[field_123], + benefits: %i[field_124], + + period: %i[field_126], + brent: %i[field_128], + scharge: %i[field_129], + pscharge: %i[field_130], + supcharg: %i[field_131], + tcharge: %i[field_132], + chcharge: %i[field_127], + household_charge: %i[field_125], + hbrentshortfall: %i[field_133], + tshortfall: %i[field_134], + + unitletas: %i[field_26], + rsnvac: %i[field_27], + sheltered: %i[field_44], + + illness_type_1: %i[field_98], + illness_type_2: %i[field_92], + illness_type_3: %i[field_95], + illness_type_4: %i[field_90], + illness_type_5: %i[field_91], + illness_type_6: %i[field_93], + illness_type_7: %i[field_94], + illness_type_8: %i[field_97], + illness_type_9: %i[field_96], + illness_type_10: %i[field_99], + + irproduct_other: %i[field_12], + + offered: %i[field_28], + propcode: %i[field_14], + + majorrepairs: %i[field_36 field_37 field_38], + mrcdate: %i[field_36 field_37 field_38], + + voiddate: %i[field_33 field_34 field_35], + } + end + + def attribute_set + @attribute_set ||= instance_variable_get(:@attributes) + end + + def questions + log.form.subsections.flat_map { |ss| ss.applicable_questions(log) } + end + + def attributes_for_log + attributes = {} + + attributes["lettype"] = field_5 + attributes["tenancycode"] = field_13 + attributes["la"] = field_25 + attributes["postcode_known"] = postcode_known + attributes["postcode_full"] = postcode_full + attributes["owning_organisation_id"] = owning_organisation_id + attributes["managing_organisation_id"] = managing_organisation_id + attributes["renewal"] = renewal + attributes["scheme"] = scheme + attributes["location"] = location + attributes["created_by"] = bulk_upload.user + attributes["needstype"] = field_4 + attributes["rent_type"] = rent_type + attributes["startdate"] = startdate + attributes["unittype_gn"] = field_29 + attributes["builtype"] = field_30 + attributes["wchair"] = field_31 + attributes["beds"] = field_32 + attributes["joint"] = field_39 + attributes["startertenancy"] = field_40 + attributes["tenancy"] = field_41 + attributes["tenancyother"] = field_42 + attributes["tenancylength"] = field_43 + attributes["declaration"] = field_45 + + attributes["age1_known"] = field_46 == "R" ? 1 : 0 + attributes["age1"] = field_46 if attributes["age1_known"].zero? && field_46&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age2_known"] = field_52 == "R" ? 1 : 0 + attributes["age2"] = field_52 if attributes["age2_known"].zero? && field_52&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age3_known"] = field_56 == "R" ? 1 : 0 + attributes["age3"] = field_56 if attributes["age3_known"].zero? && field_56&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age4_known"] = field_60 == "R" ? 1 : 0 + attributes["age4"] = field_60 if attributes["age4_known"].zero? && field_60&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age5_known"] = field_64 == "R" ? 1 : 0 + attributes["age5"] = field_64 if attributes["age5_known"].zero? && field_64&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age6_known"] = field_68 == "R" ? 1 : 0 + attributes["age6"] = field_68 if attributes["age6_known"].zero? && field_68&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age7_known"] = field_72 == "R" ? 1 : 0 + attributes["age7"] = field_72 if attributes["age7_known"].zero? && field_72&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age8_known"] = field_76 == "R" ? 1 : 0 + attributes["age8"] = field_76 if attributes["age8_known"].zero? && field_76&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["sex1"] = field_47 + attributes["sex2"] = field_53 + attributes["sex3"] = field_57 + attributes["sex4"] = field_61 + attributes["sex5"] = field_65 + attributes["sex6"] = field_69 + attributes["sex7"] = field_73 + attributes["sex8"] = field_77 + + attributes["ethnic_group"] = ethnic_group_from_ethnic + attributes["ethnic"] = field_48 + attributes["national"] = field_49 + + attributes["relat2"] = field_51 + attributes["relat3"] = field_55 + attributes["relat4"] = field_59 + attributes["relat5"] = field_63 + attributes["relat6"] = field_67 + attributes["relat7"] = field_71 + attributes["relat8"] = field_75 + + attributes["ecstat1"] = field_50 + attributes["ecstat2"] = field_54 + attributes["ecstat3"] = field_58 + attributes["ecstat4"] = field_62 + attributes["ecstat5"] = field_66 + attributes["ecstat6"] = field_70 + attributes["ecstat7"] = field_74 + attributes["ecstat8"] = field_78 + + attributes["details_known_2"] = details_known(2) + attributes["details_known_3"] = details_known(3) + attributes["details_known_4"] = details_known(4) + attributes["details_known_5"] = details_known(5) + attributes["details_known_6"] = details_known(6) + attributes["details_known_7"] = details_known(7) + attributes["details_known_8"] = details_known(8) + + attributes["armedforces"] = field_79 + attributes["leftreg"] = leftreg + attributes["reservist"] = field_81 + + attributes["preg_occ"] = field_82 + + attributes["housingneeds"] = housingneeds + attributes["housingneeds_type"] = housingneeds_type + attributes["housingneeds_other"] = housingneeds_other + + attributes["illness"] = field_89 + + attributes["layear"] = field_100 + attributes["waityear"] = field_101 + attributes["reason"] = field_102 + attributes["reasonother"] = field_103 + attributes["prevten"] = field_104 + attributes["homeless"] = homeless + + attributes["prevloc"] = prevloc + attributes["previous_la_known"] = previous_la_known + attributes["ppcodenk"] = ppcodenk + attributes["ppostcode_full"] = ppostcode_full + + attributes["reasonpref"] = field_110 + attributes["rp_homeless"] = field_111 + attributes["rp_insan_unsat"] = field_112 + attributes["rp_medwel"] = field_113 + attributes["rp_hardship"] = field_114 + attributes["rp_dontknow"] = field_115 + + attributes["cbl"] = cbl + attributes["chr"] = chr + attributes["cap"] = cap + attributes["letting_allocation_unknown"] = letting_allocation_unknown + + attributes["referral"] = field_119 + + attributes["net_income_known"] = net_income_known + attributes["earnings"] = earnings + attributes["incfreq"] = field_121 + attributes["hb"] = field_123 + attributes["benefits"] = field_124 + + attributes["period"] = field_126 + attributes["brent"] = field_128 + attributes["scharge"] = field_129 + attributes["pscharge"] = field_130 + attributes["supcharg"] = field_131 + attributes["tcharge"] = field_132 + attributes["chcharge"] = field_127 + attributes["household_charge"] = field_125 + attributes["hbrentshortfall"] = field_133 + attributes["tshortfall_known"] = tshortfall_known + attributes["tshortfall"] = field_134 + + attributes["hhmemb"] = hhmemb + + attributes["unitletas"] = field_26 + attributes["rsnvac"] = rsnvac + attributes["sheltered"] = field_44 + + attributes["illness_type_1"] = field_98 + attributes["illness_type_2"] = field_92 + attributes["illness_type_3"] = field_95 + attributes["illness_type_4"] = field_90 + attributes["illness_type_5"] = field_91 + attributes["illness_type_6"] = field_93 + attributes["illness_type_7"] = field_94 + attributes["illness_type_8"] = field_97 + attributes["illness_type_9"] = field_96 + attributes["illness_type_10"] = field_99 + + attributes["irproduct_other"] = field_12 + + attributes["offered"] = field_28 + + attributes["propcode"] = field_14 + + attributes["majorrepairs"] = majorrepairs + + attributes["mrcdate"] = mrcdate + + attributes["voiddate"] = voiddate + + attributes["first_time_property_let_as_social_housing"] = first_time_property_let_as_social_housing + + attributes + end + + def postcode_known + if postcode_full.present? + 1 + elsif field_25.present? + 0 + end + end + + def postcode_full + "#{field_23} #{field_24}" if field_23 && field_24 + end + + def owning_organisation + Organisation.find_by_id_on_mulitple_fields(field_1) + end + + def owning_organisation_id + owning_organisation&.id + end + + def managing_organisation + Organisation.find_by_id_on_mulitple_fields(field_2) + end + + def managing_organisation_id + managing_organisation&.id + end + + def renewal + case field_6 + when 1 + 1 + when 2 + 0 + when nil + rsnvac == 14 ? 1 : 0 + else + field_6 + end + end + + def rsnvac + field_27 + end + + def scheme + @scheme ||= Scheme.find_by_id_on_mulitple_fields(field_16) + end + + def location + return if scheme.nil? + + @location ||= scheme.locations.find_by_id_on_mulitple_fields(field_17) + end + + def renttype + case field_5 + when 1, 2, 3, 4 + :social + when 5, 6, 7, 8 + :affordable + when 9, 10, 11, 12 + :intermediate + end + end + + def rent_type + case renttype + when :social + Imports::LettingsLogsImportService::RENT_TYPE[:social_rent] + when :affordable + if field_10 == 1 + Imports::LettingsLogsImportService::RENT_TYPE[:london_affordable_rent] + else + Imports::LettingsLogsImportService::RENT_TYPE[:affordable_rent] + end + when :intermediate + case field_11 + when 1 + Imports::LettingsLogsImportService::RENT_TYPE[:rent_to_buy] + when 2 + Imports::LettingsLogsImportService::RENT_TYPE[:london_living_rent] + when 3 + Imports::LettingsLogsImportService::RENT_TYPE[:other_intermediate_rent_product] + end + end + end + + def startdate + Date.new(field_9 + 2000, field_8, field_7) if field_9.present? && field_8.present? && field_7.present? + rescue Date::Error + Date.new + end + + def ethnic_group_from_ethnic + return nil if field_48.blank? + + case field_48 + when 1, 2, 3, 18 + 0 + when 4, 5, 6, 7 + 1 + when 8, 9, 10, 11, 15 + 2 + when 12, 13, 14 + 3 + when 16, 19 + 4 + when 17 + 17 + end + end + + def details_known(person_n) + send("person_#{person_n}_present?") ? 0 : 1 + end + + def person_2_present? + field_51.present? && field_52.present? && field_53.present? + end + + def person_3_present? + field_55.present? && field_56.present? && field_57.present? + end + + def person_4_present? + field_59.present? && field_60.present? && field_61.present? + end + + def person_5_present? + field_63.present? && field_64.present? && field_65.present? + end + + def person_6_present? + field_67.present? && field_68.present? && field_69.present? + end + + def person_7_present? + field_71.present? && field_72.present? && field_73.present? + end + + def person_8_present? + field_75.present? && field_76.present? && field_77.present? + end + + def leftreg + case field_80 + when 3 + 3 + when 4 + 1 + when 5 + 2 + when 6 + 0 + end + end + + def housingneeds + if field_87 == 1 + 2 + elsif field_88 == 1 + 3 + else + 2 + end + end + + def housingneeds_type + if field_83 == 1 + 0 + elsif field_84 == 1 + 1 + elsif field_85 == 1 + 2 + end + end + + def housingneeds_other + return 1 if field_86 == 1 + end + + def homeless + case field_105 + when 1 + 1 + when 12 + 11 + end + end + + def prevloc + field_109 + end + + def previous_la_known + prevloc.present? ? 1 : 0 + end + + def ppcodenk + case field_106 + when 1 + 1 + when 2 + 0 + end + end + + def ppostcode_full + "#{field_107} #{field_108}".strip.gsub(/\s+/, " ") + end + + def cbl + case field_116 + when 2 + 0 + when 1 + 1 + end + end + + def chr + case field_118 + when 2 + 0 + when 1 + 1 + end + end + + def cap + case field_117 + when 2 + 0 + when 1 + 1 + end + end + + def letting_allocation_unknown + [cbl, chr, cap].all?(0) ? 1 : 0 + end + + def net_income_known + case field_120 + when 1 + 0 + when 2 + 1 + when 3 + 1 + when 4 + 2 + end + end + + def earnings + field_122.round if field_122.present? + end + + def tshortfall_known + field_133 == 1 ? 0 : 1 + end + + def hhmemb + [ + person_2_present?, + person_3_present?, + person_4_present?, + person_5_present?, + person_6_present?, + person_7_present?, + person_8_present?, + ].count(true) + 1 + end + + def majorrepairs + mrcdate.present? ? 1 : 0 + end + + def mrcdate + Date.new(field_38 + 2000, field_37, field_36) if field_38.present? && field_37.present? && field_36.present? + end + + def voiddate + Date.new(field_35 + 2000, field_34, field_33) if field_35.present? && field_34.present? && field_33.present? + end + + def first_time_property_let_as_social_housing + case rsnvac + when 15, 16, 17 + 1 + else + 0 + end + end +end diff --git a/app/services/bulk_upload/processor.rb b/app/services/bulk_upload/processor.rb index ab4fe50a7..2d27464f0 100644 --- a/app/services/bulk_upload/processor.rb +++ b/app/services/bulk_upload/processor.rb @@ -8,7 +8,7 @@ class BulkUpload::Processor def call download - return send_failure_mail if validator.invalid? + return send_failure_mail(errors: validator.errors.full_messages) if validator.invalid? validator.call @@ -62,9 +62,9 @@ private validator.create_logs? && bulk_upload.logs.group(:status).count.keys == %w[completed] end - def send_failure_mail + def send_failure_mail(errors: []) BulkUploadMailer - .send_bulk_upload_failed_service_error_mail(bulk_upload:) + .send_bulk_upload_failed_service_error_mail(bulk_upload:, errors:) .deliver_later end diff --git a/app/services/bulk_upload/sales/row_parser.rb b/app/services/bulk_upload/sales/row_parser.rb deleted file mode 100644 index f7b74dfe1..000000000 --- a/app/services/bulk_upload/sales/row_parser.rb +++ /dev/null @@ -1,176 +0,0 @@ -class BulkUpload::Sales::RowParser - include ActiveModel::Model - include ActiveModel::Attributes - - attribute :field_1, :string - attribute :field_2, :integer - attribute :field_3, :integer - attribute :field_4, :integer - attribute :field_5 - attribute :field_6, :integer - attribute :field_7, :integer - attribute :field_8, :integer - attribute :field_9, :integer - attribute :field_10, :integer - attribute :field_11, :integer - attribute :field_12, :integer - attribute :field_13, :string - attribute :field_14, :string - attribute :field_15, :string - attribute :field_16, :string - attribute :field_17, :string - attribute :field_18, :string - attribute :field_19, :string - attribute :field_20, :integer - attribute :field_21, :integer - attribute :field_22, :integer - attribute :field_23, :integer - attribute :field_24, :integer - attribute :field_25, :integer - attribute :field_26, :integer - attribute :field_27, :integer - attribute :field_28, :integer - attribute :field_29, :integer - attribute :field_30, :integer - attribute :field_31, :integer - attribute :field_32, :integer - attribute :field_33, :integer - attribute :field_34, :integer - attribute :field_35, :integer - attribute :field_36, :integer - attribute :field_37, :integer - attribute :field_38 - attribute :field_39, :integer - attribute :field_40, :string - attribute :field_41, :string - attribute :field_42, :string - attribute :field_43, :integer - attribute :field_44, :integer - attribute :field_45, :integer - attribute :field_46, :integer - attribute :field_47, :integer - attribute :field_48, :integer - attribute :field_49, :integer - attribute :field_50, :integer - attribute :field_51, :integer - attribute :field_52, :integer - attribute :field_53, :string - attribute :field_54, :string - attribute :field_55, :string - attribute :field_56, :integer - attribute :field_57, :integer - attribute :field_58, :integer - attribute :field_59, :integer - attribute :field_60, :integer - attribute :field_61, :integer - attribute :field_62, :integer - attribute :field_63, :integer - attribute :field_64, :integer - attribute :field_65, :integer - attribute :field_66, :integer - attribute :field_67, :integer - attribute :field_68, :integer - attribute :field_69, :integer - attribute :field_70, :integer - attribute :field_71, :integer - attribute :field_72, :integer - attribute :field_73, :integer - attribute :field_74, :decimal - attribute :field_75, :decimal - attribute :field_76, :integer - attribute :field_77, :integer - attribute :field_78, :integer - attribute :field_79, :integer - attribute :field_80, :integer - attribute :field_81, :integer - attribute :field_82, :integer - attribute :field_83, :integer - attribute :field_84, :integer - attribute :field_85, :string - attribute :field_86 - attribute :field_87, :integer - attribute :field_88, :integer - attribute :field_89, :integer - attribute :field_90, :integer - attribute :field_91, :integer - attribute :field_92, :integer - attribute :field_93, :string - attribute :field_94 - attribute :field_95, :integer - attribute :field_96 - attribute :field_97, :integer - attribute :field_98, :integer - attribute :field_99, :string - attribute :field_100, :integer - attribute :field_101, :string - attribute :field_102, :integer - attribute :field_103, :string - attribute :field_104, :integer - attribute :field_105, :integer - attribute :field_106, :integer - attribute :field_107, :integer - attribute :field_108, :integer - attribute :field_109, :integer - attribute :field_110, :integer - attribute :field_111, :integer - attribute :field_112, :integer - attribute :field_113, :integer - attribute :field_114, :integer - attribute :field_115, :integer - attribute :field_116, :integer - attribute :field_117, :integer - attribute :field_118, :integer - attribute :field_119, :integer - attribute :field_120, :integer - attribute :field_121, :integer - attribute :field_122, :integer - attribute :field_123, :integer - attribute :field_124, :integer - attribute :field_125, :integer - - # validates :field_1, presence: true, numericality: { in: (1..12) } - # validates :field_4, numericality: { in: (1..999), allow_blank: true } - # validates :field_4, presence: true, if: :field_4_presence_check - - validate :validate_possible_answers - -# delegate :valid?, to: :native_object -# delegate :errors, to: :native_object - -private - - def native_object - @native_object ||= SalesLog.new(attributes_for_log) - end - - def field_mapping - { - field_117: :buy1livein, - } - end - - def validate_possible_answers - field_mapping.each do |field, attribute| - possible_answers = FormHandler.instance.current_sales_form.questions.find { |q| q.id == attribute.to_s }.answer_options.keys - - unless possible_answers.include?(public_send(field)) - errors.add(field, "Value supplied is not one of the permitted values") - end - end - end - - def attributes_for_log - hash = field_mapping.invert - attributes = {} - - hash.map do |k, v| - attributes[k] = public_send(v) - end - - attributes - end - - # def field_4_presence_check - # [1, 3, 5, 7, 9, 11].include?(field_1) - # end -end diff --git a/app/services/bulk_upload/sales/validator.rb b/app/services/bulk_upload/sales/validator.rb index 43b8c001d..bddbfd13f 100644 --- a/app/services/bulk_upload/sales/validator.rb +++ b/app/services/bulk_upload/sales/validator.rb @@ -1,138 +1,6 @@ class BulkUpload::Sales::Validator include ActiveModel::Validations - QUESTIONS = { - field_1: "What is the purchaser code?", - field_2: "What is the day of the sale completion date? - DD", - field_3: "What is the month of the sale completion date? - MM", - field_4: "What is the year of the sale completion date? - YY", - field_5: "This question has been removed", - field_6: "Was the buyer interviewed for any of the answers you will provide on this log?", - field_7: "Age of Buyer 1", - field_8: "Age of Person 2", - field_9: "Age of Person 3", - field_10: "Age of Person 4", - field_11: "Age of Person 5", - field_12: "Age of Person 6", - field_13: "Gender identity of Buyer 1", - field_14: "Gender identity of Person 2", - field_15: "Gender identity of Person 3", - field_16: "Gender identity of Person 4", - field_17: "Gender identity of Person 5", - field_18: "Gender identity of Person 6", - field_19: "Relationship to Buyer 1 for Person 2", - field_20: "Relationship to Buyer 1 for Person 3", - field_21: "Relationship to Buyer 1 for Person 4", - field_22: "Relationship to Buyer 1 for Person 5", - field_23: "Relationship to Buyer 1 for Person 6", - field_24: "Working situation of Buyer 1", - field_25: "Working situation of Person 2", - field_26: "Working situation of Person 3", - field_27: "Working situation of Person 4", - field_28: "Working situation of Person 5", - field_29: "Working situation of Person 6", - field_30: "What is buyer 1's ethnic group?", - field_31: "What is buyer 1's nationality?", - field_32: "What is buyer 1's gross annual income?", - field_33: "What is buyer 2's gross annual income?", - field_34: "Was buyer 1's income used for a mortgage application?", - field_35: "Was buyer 2's income used for a mortgage application?", - field_36: "What is the total amount the buyers had in savings before they paid any deposit for the property?", - field_37: "Have any of the purchasers previously owned a property?", - field_38: "This question has been removed", - field_39: "What was buyer 1's previous tenure?", - field_40: "What is the local authority of buyer 1's last settled home?", - field_41: "Part 1 of postcode of buyer 1's last settled home", - field_42: "Part 2 of postcode of buyer 1's last settled home", - field_43: "Do you know the postcode of buyer 1's last settled home?", - field_44: "Was the buyer registered with their PRP (HA)?", - field_45: "Was the buyer registered with the local authority?", - field_46: "Was the buyer registered with a Help to Buy agent?", - field_47: "Was the buyer registered with another PRP (HA)?", - field_48: "Does anyone in the household consider themselves to have a disability?", - field_49: "Does anyone in the household use a wheelchair?", - field_50: "How many bedrooms does the property have?", - field_51: "What type of unit is the property?", - field_52: "Which type of bulding is the property?", - field_53: "What is the local authority of the property?", - field_54: "Part 1 of postcode of property", - field_55: "Part 2 of postcode of property", - field_56: "Is the property built or adapted to wheelchair user standards?", - field_57: "What is the type of shared ownership sale?", - field_58: "Is this a resale?", - field_59: "What is the day of the practical completion or handover date?", - field_60: "What is the month of the practical completion or handover date?", - field_61: "What is the day of the exchange of contracts date?", - field_62: "What is the day of the practical completion or handover date?", - field_63: "What is the month of the practical completion or handover date?", - field_64: "What is the year of the practical completion or handover date?", - field_65: "Was the household re-housed under a local authority nominations agreement?", - field_66: "How many bedrooms did the buyer's previous property have?", - field_67: "What was the type of the buyer's previous property?", - field_68: "What was the full purchase price?", - field_69: "What was the initial percentage equity stake purchased?", - field_70: "What is the mortgage amount?", - field_71: "Does this include any extra borrowing?", - field_72: "How much was the cash deposit paid on the property?", - field_73: "How much cash discount was given through Social Homebuy?", - field_74: "What is the basic monthly rent?", - field_75: "What are the total monthly leasehold charges for the property?", - field_76: "What is the type of discounted ownership sale?", - field_77: "What was the full purchase price?", - field_78: "What was the amount of any loan, grant, discount or subsidy given?", - field_79: "What was the percentage discount?", - field_80: "What is the mortgage amount?", - field_81: "Does this include any extra borrowing?", - field_82: "How much was the cash deposit paid on the property?", - field_83: "What are the total monthly leasehold charges for the property?", - field_84: "What is the type of outright sale?", - field_85: "If 'other', what is the 'other' type?", - field_86: "This question has been removed", - field_87: "What is the full purchase price?", - field_88: "What is the mortgage amount?", - field_89: "Does this include any extra borrowing?", - field_90: "How much was the cash deposit paid on the property?", - field_91: "What are the total monthly leasehold charges for the property?", - field_92: "Which organisation owned this property before the sale?", - field_93: "Username", - field_94: "This question has been removed", - field_95: "Has the buyer ever served in the UK Armed Forces and for how long?", - field_96: "This question has been removed", - field_97: "Are any of the buyers a spouse or civil partner of a UK Armed Forces regular who died in service within the last 2 years?", - field_98: "What is the name of the mortgage lender? - Shared ownership", - field_99: "If 'other', what is the name of the mortgage lender?", - field_100: "What is the name of the mortgage lender? - Discounted ownership", - field_101: "If 'other', what is the name of the mortgage lender?", - field_102: "What is the name of the mortgage lender? - Outright sale", - field_103: "If 'other', what is the name of the mortgage lender?", - field_104: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?", - field_105: "What is the length of the mortgage in years? - Shared ownership", - field_106: "What is the length of the mortgage in years? - Discounted ownership", - field_107: "What is the length of the mortgage in years? - Outright sale", - field_108: "How long have the buyers been living in the property before the purchase? - Discounted ownership", - field_109: "Are there more than two joint purchasers of this property?", - field_110: "How long have the buyers been living in the property before the purchase? - Shared ownership", - field_111: "Is this a staircasing transaction?", - field_112: "Data Protection question", - field_113: "Was this purchase made through an ownership scheme?", - field_114: "Is the buyer a company?", - field_115: "Will the buyers live in the property?", - field_116: "Is this a joint purchase?", - field_117: "Will buyer 1 live in the property?", - field_118: "Will buyer 2 live in the property?", - field_119: "Besides the buyers, how many people will live in the property?", - field_120: "What percentage of the property has been bought in this staircasing transaction?", - field_121: "What percentage of the property does the buyer now own in total?", - field_122: "What was the rent type of the buyer's previous property?", - field_123: "Was a mortgage used for the purchase of this property? - Shared ownership", - field_124: "Was a mortgage used for the purchase of this property? - Discounted ownership", - field_125: "Was a mortgage used for the purchase of this property? - Outright sale", - }.freeze - - def self.question_for_field(field) - QUESTIONS[field] - end - attr_reader :bulk_upload, :path validate :validate_file_not_empty @@ -193,7 +61,7 @@ private headers = ("field_1".."field_125").to_a hash = Hash[headers.zip(stripped_row)] - BulkUpload::Sales::RowParser.new(hash) + BulkUpload::Sales::Year2022::RowParser.new(hash) end end diff --git a/app/services/bulk_upload/sales/year2022/row_parser.rb b/app/services/bulk_upload/sales/year2022/row_parser.rb new file mode 100644 index 000000000..965c3547a --- /dev/null +++ b/app/services/bulk_upload/sales/year2022/row_parser.rb @@ -0,0 +1,308 @@ +class BulkUpload::Sales::Year2022::RowParser + include ActiveModel::Model + include ActiveModel::Attributes + + QUESTIONS = { + field_1: "What is the purchaser code?", + field_2: "What is the day of the sale completion date? - DD", + field_3: "What is the month of the sale completion date? - MM", + field_4: "What is the year of the sale completion date? - YY", + field_5: "This question has been removed", + field_6: "Was the buyer interviewed for any of the answers you will provide on this log?", + field_7: "Age of Buyer 1", + field_8: "Age of Person 2", + field_9: "Age of Person 3", + field_10: "Age of Person 4", + field_11: "Age of Person 5", + field_12: "Age of Person 6", + field_13: "Gender identity of Buyer 1", + field_14: "Gender identity of Person 2", + field_15: "Gender identity of Person 3", + field_16: "Gender identity of Person 4", + field_17: "Gender identity of Person 5", + field_18: "Gender identity of Person 6", + field_19: "Relationship to Buyer 1 for Person 2", + field_20: "Relationship to Buyer 1 for Person 3", + field_21: "Relationship to Buyer 1 for Person 4", + field_22: "Relationship to Buyer 1 for Person 5", + field_23: "Relationship to Buyer 1 for Person 6", + field_24: "Working situation of Buyer 1", + field_25: "Working situation of Person 2", + field_26: "Working situation of Person 3", + field_27: "Working situation of Person 4", + field_28: "Working situation of Person 5", + field_29: "Working situation of Person 6", + field_30: "What is buyer 1's ethnic group?", + field_31: "What is buyer 1's nationality?", + field_32: "What is buyer 1's gross annual income?", + field_33: "What is buyer 2's gross annual income?", + field_34: "Was buyer 1's income used for a mortgage application?", + field_35: "Was buyer 2's income used for a mortgage application?", + field_36: "What is the total amount the buyers had in savings before they paid any deposit for the property?", + field_37: "Have any of the purchasers previously owned a property?", + field_38: "This question has been removed", + field_39: "What was buyer 1's previous tenure?", + field_40: "What is the local authority of buyer 1's last settled home?", + field_41: "Part 1 of postcode of buyer 1's last settled home", + field_42: "Part 2 of postcode of buyer 1's last settled home", + field_43: "Do you know the postcode of buyer 1's last settled home?", + field_44: "Was the buyer registered with their PRP (HA)?", + field_45: "Was the buyer registered with the local authority?", + field_46: "Was the buyer registered with a Help to Buy agent?", + field_47: "Was the buyer registered with another PRP (HA)?", + field_48: "Does anyone in the household consider themselves to have a disability?", + field_49: "Does anyone in the household use a wheelchair?", + field_50: "How many bedrooms does the property have?", + field_51: "What type of unit is the property?", + field_52: "Which type of bulding is the property?", + field_53: "What is the local authority of the property?", + field_54: "Part 1 of postcode of property", + field_55: "Part 2 of postcode of property", + field_56: "Is the property built or adapted to wheelchair user standards?", + field_57: "What is the type of shared ownership sale?", + field_58: "Is this a resale?", + field_59: "What is the day of the practical completion or handover date?", + field_60: "What is the month of the practical completion or handover date?", + field_61: "What is the day of the exchange of contracts date?", + field_62: "What is the day of the practical completion or handover date?", + field_63: "What is the month of the practical completion or handover date?", + field_64: "What is the year of the practical completion or handover date?", + field_65: "Was the household re-housed under a local authority nominations agreement?", + field_66: "How many bedrooms did the buyer's previous property have?", + field_67: "What was the type of the buyer's previous property?", + field_68: "What was the full purchase price?", + field_69: "What was the initial percentage equity stake purchased?", + field_70: "What is the mortgage amount?", + field_71: "Does this include any extra borrowing?", + field_72: "How much was the cash deposit paid on the property?", + field_73: "How much cash discount was given through Social Homebuy?", + field_74: "What is the basic monthly rent?", + field_75: "What are the total monthly leasehold charges for the property?", + field_76: "What is the type of discounted ownership sale?", + field_77: "What was the full purchase price?", + field_78: "What was the amount of any loan, grant, discount or subsidy given?", + field_79: "What was the percentage discount?", + field_80: "What is the mortgage amount?", + field_81: "Does this include any extra borrowing?", + field_82: "How much was the cash deposit paid on the property?", + field_83: "What are the total monthly leasehold charges for the property?", + field_84: "What is the type of outright sale?", + field_85: "If 'other', what is the 'other' type?", + field_86: "This question has been removed", + field_87: "What is the full purchase price?", + field_88: "What is the mortgage amount?", + field_89: "Does this include any extra borrowing?", + field_90: "How much was the cash deposit paid on the property?", + field_91: "What are the total monthly leasehold charges for the property?", + field_92: "Which organisation owned this property before the sale?", + field_93: "Username", + field_94: "This question has been removed", + field_95: "Has the buyer ever served in the UK Armed Forces and for how long?", + field_96: "This question has been removed", + field_97: "Are any of the buyers a spouse or civil partner of a UK Armed Forces regular who died in service within the last 2 years?", + field_98: "What is the name of the mortgage lender? - Shared ownership", + field_99: "If 'other', what is the name of the mortgage lender?", + field_100: "What is the name of the mortgage lender? - Discounted ownership", + field_101: "If 'other', what is the name of the mortgage lender?", + field_102: "What is the name of the mortgage lender? - Outright sale", + field_103: "If 'other', what is the name of the mortgage lender?", + field_104: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?", + field_105: "What is the length of the mortgage in years? - Shared ownership", + field_106: "What is the length of the mortgage in years? - Discounted ownership", + field_107: "What is the length of the mortgage in years? - Outright sale", + field_108: "How long have the buyers been living in the property before the purchase? - Discounted ownership", + field_109: "Are there more than two joint purchasers of this property?", + field_110: "How long have the buyers been living in the property before the purchase? - Shared ownership", + field_111: "Is this a staircasing transaction?", + field_112: "Data Protection question", + field_113: "Was this purchase made through an ownership scheme?", + field_114: "Is the buyer a company?", + field_115: "Will the buyers live in the property?", + field_116: "Is this a joint purchase?", + field_117: "Will buyer 1 live in the property?", + field_118: "Will buyer 2 live in the property?", + field_119: "Besides the buyers, how many people will live in the property?", + field_120: "What percentage of the property has been bought in this staircasing transaction?", + field_121: "What percentage of the property does the buyer now own in total?", + field_122: "What was the rent type of the buyer's previous property?", + field_123: "Was a mortgage used for the purchase of this property? - Shared ownership", + field_124: "Was a mortgage used for the purchase of this property? - Discounted ownership", + field_125: "Was a mortgage used for the purchase of this property? - Outright sale", + }.freeze + + attribute :field_1, :string + attribute :field_2, :integer + attribute :field_3, :integer + attribute :field_4, :integer + attribute :field_5 + attribute :field_6, :integer + attribute :field_7, :integer + attribute :field_8, :integer + attribute :field_9, :integer + attribute :field_10, :integer + attribute :field_11, :integer + attribute :field_12, :integer + attribute :field_13, :string + attribute :field_14, :string + attribute :field_15, :string + attribute :field_16, :string + attribute :field_17, :string + attribute :field_18, :string + attribute :field_19, :string + attribute :field_20, :integer + attribute :field_21, :integer + attribute :field_22, :integer + attribute :field_23, :integer + attribute :field_24, :integer + attribute :field_25, :integer + attribute :field_26, :integer + attribute :field_27, :integer + attribute :field_28, :integer + attribute :field_29, :integer + attribute :field_30, :integer + attribute :field_31, :integer + attribute :field_32, :integer + attribute :field_33, :integer + attribute :field_34, :integer + attribute :field_35, :integer + attribute :field_36, :integer + attribute :field_37, :integer + attribute :field_38 + attribute :field_39, :integer + attribute :field_40, :string + attribute :field_41, :string + attribute :field_42, :string + attribute :field_43, :integer + attribute :field_44, :integer + attribute :field_45, :integer + attribute :field_46, :integer + attribute :field_47, :integer + attribute :field_48, :integer + attribute :field_49, :integer + attribute :field_50, :integer + attribute :field_51, :integer + attribute :field_52, :integer + attribute :field_53, :string + attribute :field_54, :string + attribute :field_55, :string + attribute :field_56, :integer + attribute :field_57, :integer + attribute :field_58, :integer + attribute :field_59, :integer + attribute :field_60, :integer + attribute :field_61, :integer + attribute :field_62, :integer + attribute :field_63, :integer + attribute :field_64, :integer + attribute :field_65, :integer + attribute :field_66, :integer + attribute :field_67, :integer + attribute :field_68, :integer + attribute :field_69, :integer + attribute :field_70, :integer + attribute :field_71, :integer + attribute :field_72, :integer + attribute :field_73, :integer + attribute :field_74, :decimal + attribute :field_75, :decimal + attribute :field_76, :integer + attribute :field_77, :integer + attribute :field_78, :integer + attribute :field_79, :integer + attribute :field_80, :integer + attribute :field_81, :integer + attribute :field_82, :integer + attribute :field_83, :integer + attribute :field_84, :integer + attribute :field_85, :string + attribute :field_86 + attribute :field_87, :integer + attribute :field_88, :integer + attribute :field_89, :integer + attribute :field_90, :integer + attribute :field_91, :integer + attribute :field_92, :integer + attribute :field_93, :string + attribute :field_94 + attribute :field_95, :integer + attribute :field_96 + attribute :field_97, :integer + attribute :field_98, :integer + attribute :field_99, :string + attribute :field_100, :integer + attribute :field_101, :string + attribute :field_102, :integer + attribute :field_103, :string + attribute :field_104, :integer + attribute :field_105, :integer + attribute :field_106, :integer + attribute :field_107, :integer + attribute :field_108, :integer + attribute :field_109, :integer + attribute :field_110, :integer + attribute :field_111, :integer + attribute :field_112, :integer + attribute :field_113, :integer + attribute :field_114, :integer + attribute :field_115, :integer + attribute :field_116, :integer + attribute :field_117, :integer + attribute :field_118, :integer + attribute :field_119, :integer + attribute :field_120, :integer + attribute :field_121, :integer + attribute :field_122, :integer + attribute :field_123, :integer + attribute :field_124, :integer + attribute :field_125, :integer + + # validates :field_1, presence: true, numericality: { in: (1..12) } + # validates :field_4, numericality: { in: (1..999), allow_blank: true } + # validates :field_4, presence: true, if: :field_4_presence_check + + validate :validate_possible_answers + + # delegate :valid?, to: :native_object + # delegate :errors, to: :native_object + + def self.question_for_field(field) + QUESTIONS[field] + end + +private + + def native_object + @native_object ||= SalesLog.new(attributes_for_log) + end + + def field_mapping + { + field_117: :buy1livein, + } + end + + def validate_possible_answers + field_mapping.each do |field, attribute| + possible_answers = FormHandler.instance.current_sales_form.questions.find { |q| q.id == attribute.to_s }.answer_options.keys + + unless possible_answers.include?(public_send(field)) + errors.add(field, "Value supplied is not one of the permitted values") + end + end + end + + def attributes_for_log + hash = field_mapping.invert + attributes = {} + + hash.map do |k, v| + attributes[k] = public_send(v) + end + + attributes + end + + # def field_4_presence_check + # [1, 3, 5, 7, 9, 11].include?(field_1) + # end +end diff --git a/app/services/csv/lettings_log_csv_service.rb b/app/services/csv/lettings_log_csv_service.rb index c8a2cf41e..09d373b01 100644 --- a/app/services/csv/lettings_log_csv_service.rb +++ b/app/services/csv/lettings_log_csv_service.rb @@ -2,8 +2,9 @@ module Csv class LettingsLogCsvService CSV_FIELDS_TO_OMIT = %w[hhmemb net_income_value_check first_time_property_let_as_social_housing renttype needstype postcode_known is_la_inferred totchild totelder totadult net_income_known is_carehome previous_la_known is_previous_la_inferred age1_known age2_known age3_known age4_known age5_known age6_known age7_known age8_known letting_allocation_unknown details_known_2 details_known_3 details_known_4 details_known_5 details_known_6 details_known_7 details_known_8 rent_type_detail wrent wscharge wpschrge wsupchrg wtcharge wtshortfall rent_value_check old_form_id old_id retirement_value_check tshortfall_known pregnancy_value_check hhtype new_old vacdays la prevloc unresolved updated_by_id bulk_upload_id].freeze - def initialize(user) + def initialize(user, export_type:) @user = user + @export_type = export_type set_csv_attributes end @@ -12,30 +13,126 @@ module Csv csv << @attributes LettingsLog.all.find_each do |record| - csv << @attributes.map do |att| - label_from_value(record, att) - end + csv << @attributes.map { |attribute| get_value(attribute, record) } end end end private - def label_from_value(record, att) - if %w[la prevloc].include? att - label_from_boolean_value(record.send(att)) - elsif %w[mrcdate startdate voiddate].include? att - record.send(att)&.to_formatted_s(:govuk_date) + ATTRIBUTES_OF_RELATED_OBJECTS = { + location_code: { + labels: %i[location id], + codes: %i[location id], + }, + location_postcode: { + labels: %i[location postcode], + codes: %i[location postcode], + }, + location_name: { + labels: %i[location name], + codes: %i[location name], + }, + location_units: { + labels: %i[location units], + codes: %i[location units], + }, + location_type_of_unit: { + labels: %i[location type_of_unit], + codes: %i[location type_of_unit_before_type_cast], + }, + location_mobility_type: { + labels: %i[location mobility_type], + codes: %i[location mobility_type_before_type_cast], + }, + location_admin_district: { + labels: %i[location location_admin_district], + codes: %i[location location_admin_district], + }, + location_startdate: { + labels: %i[location startdate], + codes: %i[location startdate], + }, + scheme_service_name: { + labels: %i[scheme service_name], + codes: %i[scheme service_name], + }, + scheme_sensitive: { + labels: %i[scheme sensitive], + codes: %i[scheme sensitive_before_type_cast], + }, + scheme_type: { + labels: %i[scheme scheme_type], + codes: %i[scheme scheme_type_before_type_cast], + }, + scheme_registered_under_care_act: { + labels: %i[scheme registered_under_care_act], + codes: %i[scheme registered_under_care_act_before_type_cast], + }, + scheme_owning_organisation_name: { + labels: %i[scheme owning_organisation name], + codes: %i[scheme owning_organisation name], + }, + scheme_primary_client_group: { + labels: %i[scheme primary_client_group], + codes: %i[scheme primary_client_group_before_type_cast], + }, + scheme_has_other_client_group: { + labels: %i[scheme has_other_client_group], + codes: %i[scheme has_other_client_group_before_type_cast], + }, + scheme_secondary_client_group: { + labels: %i[scheme secondary_client_group], + codes: %i[scheme secondary_client_group_before_type_cast], + }, + scheme_support_type: { + labels: %i[scheme support_type], + codes: %i[scheme support_type_before_type_cast], + }, + scheme_intended_stay: { + labels: %i[scheme intended_stay], + codes: %i[scheme intended_stay_before_type_cast], + }, + scheme_created_at: { + labels: %i[scheme created_at], + codes: %i[scheme created_at], + }, + }.freeze + + def get_value(attribute, record) + attribute = "rent_type" if attribute == "rent_type_detail" # rent_type_detail is the requested column header for rent_type, so as not to confuse with renttype + if ATTRIBUTES_OF_RELATED_OBJECTS.key? attribute.to_sym + call_chain = ATTRIBUTES_OF_RELATED_OBJECTS[attribute.to_sym][@export_type.to_sym] + call_chain.reduce(record) { |object, next_call| object&.send(next_call) } + elsif %w[la prevloc].include? attribute # for all exports we output both the codes and labels for these location attributes + record.send(attribute) + elsif %w[la_label prevloc_label].include? attribute # as above + attribute = attribute.remove("_label") + field_value = record.send(attribute) + get_label(field_value, attribute, record) + elsif %w[mrcdate startdate voiddate].include? attribute + record.send(attribute)&.to_formatted_s(:govuk_date) else - record.form.get_question(att.remove("_label"), record)&.label_from_value(record.send(att.remove("_label"))) || label_from_boolean_value(record.send(att.remove("_label"))) + field_value = record.send(attribute) + case @export_type + when "codes" + field_value + when "labels" + answer_label = get_label(field_value, attribute, record) + answer_label || label_if_boolean_value(field_value) || field_value + end end end - def label_from_boolean_value(value) + def get_label(value, attribute, record) + record.form + .get_question(attribute, record) + &.label_from_value(value) + end + + def label_if_boolean_value(value) return "Yes" if value == true return "No" if value == false - - value end def set_csv_attributes diff --git a/app/services/imports/sales_logs_import_service.rb b/app/services/imports/sales_logs_import_service.rb index ed127bff2..577e37e5f 100644 --- a/app/services/imports/sales_logs_import_service.rb +++ b/app/services/imports/sales_logs_import_service.rb @@ -54,7 +54,6 @@ module Imports attributes["details_known_#{index}"] = details_known(index, attributes) end attributes["national"] = unsafe_string_as_integer(xml_doc, "P1Nat") - attributes["othernational"] = nil attributes["ethnic"] = unsafe_string_as_integer(xml_doc, "P1Eth") attributes["ethnic_group"] = ethnic_group(attributes["ethnic"]) attributes["buy1livein"] = unsafe_string_as_integer(xml_doc, "LiveInBuyer1") diff --git a/app/views/layouts/_collection_resources.html.erb b/app/views/layouts/_collection_resources.html.erb index 726cd1a86..37e9c4b94 100644 --- a/app/views/layouts/_collection_resources.html.erb +++ b/app/views/layouts/_collection_resources.html.erb @@ -6,8 +6,8 @@ <%= render DocumentListComponent.new(items: [ { name: "Lettings log for tenants (2023/24)", - href: "https://core.communities.gov.uk/public/download/guides-and-manuals/2023-24%20Lettings%20paper%20form.pdf?download-format=pdf", - metadata: "PDF, 281 KB, 8 pages", + href: download_23_24_lettings_form_path, + metadata: "PDF, 278 KB, 8 pages", }, ]) %> @@ -19,4 +19,22 @@ metadata: "PDF, 654 KB, 4 pages", }, ]) %> + +

Sales 23/24

+ <%= render DocumentListComponent.new(items: [ + { + name: "Sales log for buyers (2023/24)", + href: download_23_24_sales_form_path, + metadata: "PDF, 421 KB, 8 pages", + }, + ]) %> + +

Sales 22/23

+ <%= render DocumentListComponent.new(items: [ + { + name: "Sales log for buyers (2022/23)", + href: download_22_23_sales_form_path, + metadata: "PDF, 397 KB, 5 pages", + }, + ]) %> diff --git a/app/views/logs/_log_list.html.erb b/app/views/logs/_log_list.html.erb index 1aeaa03f2..9112cde81 100644 --- a/app/views/logs/_log_list.html.erb +++ b/app/views/logs/_log_list.html.erb @@ -1,7 +1,10 @@

<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "logs", path: request.path)) %> <% if logs&.first&.lettings? %> - <%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv" %> + <%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4" %> + <% if @current_user.support? %> + <%= govuk_link_to "Download (CSV, codes only)", csv_codes_only_download_url, type: "text/csv" %> + <% end %> <% end %>

<% logs.map do |log| %> diff --git a/app/views/logs/download_csv.html.erb b/app/views/logs/download_csv.html.erb index 4e0da2704..5aeea8173 100644 --- a/app/views/logs/download_csv.html.erb +++ b/app/views/logs/download_csv.html.erb @@ -11,6 +11,6 @@

We'll send a secure download link to your email address <%= @current_user.email %>.

You've selected <%= count %> logs.

- <%= govuk_button_to "Send email", post_path, method: :post, params: { search: search_term } %> + <%= govuk_button_to "Send email", post_path, method: :post, params: { search: search_term, codes_only: } %> diff --git a/app/views/logs/index.html.erb b/app/views/logs/index.html.erb index 0e4cd76df..2d88c9bd6 100644 --- a/app/views/logs/index.html.erb +++ b/app/views/logs/index.html.erb @@ -66,7 +66,17 @@
<%= 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_url_for_controller(controller) } %> + <%= 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_type: controller, search: @search_term, codes_only: false), + csv_codes_only_download_url: csv_download_url_for_controller(controller_type: controller, search: @search_term, codes_only: true), + } %> <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
diff --git a/app/views/organisations/logs.html.erb b/app/views/organisations/logs.html.erb index 94fe8e9bc..bac737e6c 100644 --- a/app/views/organisations/logs.html.erb +++ b/app/views/organisations/logs.html.erb @@ -26,7 +26,17 @@
<%= render SearchComponent.new(current_user:, search_label: "Search by log ID, tenant code, property reference or postcode", value: @searched) %> <%= govuk_section_break(visible: true, size: "m") %> - <%= render partial: "logs/log_list", locals: { logs: @logs, title: "Logs", pagy: @pagy, searched: @searched, item_label:, total_count: @total_count, csv_download_url: logs_csv_download_organisation_path(@organisation, search: @search_term) } %> + <%= render partial: "logs/log_list", + locals: { + logs: @logs, + title: "Logs", + pagy: @pagy, + searched: @searched, + item_label:, + total_count: @total_count, + csv_download_url: logs_csv_download_organisation_path(@organisation, search: @search_term, codes_only: false), + csv_codes_only_download_url: logs_csv_download_organisation_path(@organisation, search: @search_term, codes_only: true), + } %> <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
diff --git a/config/forms/2022_2023.json b/config/forms/2022_2023.json index e0ef19198..eeed6c3c7 100644 --- a/config/forms/2022_2023.json +++ b/config/forms/2022_2023.json @@ -1110,9 +1110,6 @@ }, { "tenancy": 6 - }, - { - "tenancy": 3 } ] }, diff --git a/config/initializers/feature_toggle.rb b/config/initializers/feature_toggle.rb index 4fc402212..0253bcc41 100644 --- a/config/initializers/feature_toggle.rb +++ b/config/initializers/feature_toggle.rb @@ -40,6 +40,12 @@ class FeatureToggle !Rails.env.development? end + def self.force_crossover? + return false if Rails.env.test? + + !Rails.env.production? + end + def self.validate_valid_radio_options? !(Rails.env.production? || Rails.env.staging?) end diff --git a/config/locales/en.yml b/config/locales/en.yml index 9c8324d34..e17c0ab9b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -41,6 +41,11 @@ en: activemodel: errors: models: + bulk_upload/lettings/validator: + attributes: + base: + over_max_column_count: "too many columns, please ensure you have used the correct template" + under_min_column_count: "too few columns, please ensure you have used the correct template" forms/bulk_upload_lettings/year: attributes: year: @@ -154,6 +159,8 @@ en: Enter a date within the %{current_start_year_short}/%{current_end_year_short} financial year, which is between %{current_start_year_long} and %{current_end_year_long} previous_and_current_financial_year: "Enter a date within the %{previous_start_year_short}/%{previous_end_year_short} or %{previous_end_year_short}/%{current_end_year_short} financial years, which is between %{previous_start_year_long} and %{current_end_year_long}" + type: + percentage_bought_must_be_at_least_threshold: "The minimum increase in equity while staircasing is %{threshold}% for this shared ownership type" startdate: later_than_14_days_after: "The tenancy start date must not be later than 14 days from today’s date" @@ -297,6 +304,7 @@ en: staircasing: percentage_bought_must_be_greater_than_percentage_owned: "Total percentage buyer now owns must be more than percentage bought in this transaction" older_person_percentage_owned_maximum_75: "Percentage cannot be above 75% under Older Person's Shared Ownership" + percentage_bought_must_be_at_least_threshold: "The minimum increase in equity while staircasing is %{threshold}%" household: reasonpref: diff --git a/config/rent_range_data/2023.csv b/config/rent_range_data/2023.csv index 08775d4a4..5a0d9858f 100644 --- a/config/rent_range_data/2023.csv +++ b/config/rent_range_data/2023.csv @@ -1,4 +1,4 @@ -ranges_rent_id,lettype,la,beds,soft_min,soft_max,hard_min,hard_max +ranges_rent_id,lettype,la,beds,soft_min,soft_max,hard_min,hard_max 1,1,E07000223,1,57.21,123.09,25.84,176.15 327,1,E07000223,2,71.06,146.43,25.84,186.77 653,1,E07000223,3,81.2,161.3,25.84,203.75 diff --git a/config/routes.rb b/config/routes.rb index 02a9cb707..9c66098d7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,9 @@ Rails.application.routes.draw do get "/accessibility-statement", to: "content#accessibility_statement" get "/privacy-notice", to: "content#privacy_notice" get "/data-sharing-agreement", to: "content#data_sharing_agreement" + get "/download-23-24-sales-form", to: "start#download_23_24_sales_form" + get "/download-22-23-sales-form", to: "start#download_22_23_sales_form" + get "/download-23-24-lettings-form", to: "start#download_23_24_lettings_form" resource :account, only: %i[show edit], controller: "users" do get "edit/password", to: "users#edit_password" diff --git a/db/migrate/20230224083552_add_columns_to_sales_log.rb b/db/migrate/20230224083552_add_columns_to_sales_log.rb new file mode 100644 index 000000000..f4fbc91df --- /dev/null +++ b/db/migrate/20230224083552_add_columns_to_sales_log.rb @@ -0,0 +1,8 @@ +class AddColumnsToSalesLog < ActiveRecord::Migration[7.0] + def change + change_table :sales_logs, bulk: true do |t| + t.column :buy2living, :integer + t.column :prevtenbuy2, :integer + end + end +end diff --git a/db/migrate/20230307111943_remove_othernational_from_sales_logs.rb b/db/migrate/20230307111943_remove_othernational_from_sales_logs.rb new file mode 100644 index 000000000..534962773 --- /dev/null +++ b/db/migrate/20230307111943_remove_othernational_from_sales_logs.rb @@ -0,0 +1,5 @@ +class RemoveOthernationalFromSalesLogs < ActiveRecord::Migration[7.0] + def change + remove_column :sales_logs, :othernational, :string + end +end diff --git a/db/migrate/20230307140937_add_nationalbuy2_to_sales_logs.rb b/db/migrate/20230307140937_add_nationalbuy2_to_sales_logs.rb new file mode 100644 index 000000000..f49f2c493 --- /dev/null +++ b/db/migrate/20230307140937_add_nationalbuy2_to_sales_logs.rb @@ -0,0 +1,5 @@ +class AddNationalbuy2ToSalesLogs < ActiveRecord::Migration[7.0] + def change + add_column :sales_logs, :nationalbuy2, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 3b26fde33..085419473 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -403,7 +403,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_08_101826) do t.integer "age1_known" t.string "sex1" t.integer "national" - t.string "othernational" t.integer "ethnic" t.integer "ethnic_group" t.integer "buy1livein" @@ -543,6 +542,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_08_101826) do t.integer "proplen_asked" t.string "old_id" t.integer "pregblank" + t.integer "buy2living" + t.integer "prevtenbuy2" + t.integer "nationalbuy2" t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id" t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id" t.index ["old_id"], name: "index_sales_logs_on_old_id", unique: true diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index c8a433b8f..52a7b091a 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.4.2) + activesupport (7.0.4.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -14,7 +14,7 @@ GEM coffee-script-source (1.11.1) colorator (1.1.0) commonmarker (0.23.8) - concurrent-ruby (1.2.0) + concurrent-ruby (1.2.2) dnsruby (1.61.9) simpleidn (~> 0.1) em-websocket (0.5.3) @@ -209,7 +209,7 @@ GEM jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.17.0) + minitest (5.18.0) nokogiri (1.14.2-arm64-darwin) racc (~> 1.4) nokogiri (1.14.2-x86_64-darwin) diff --git a/public/files/2022_23_sales_paper_form.pdf b/public/files/2022_23_sales_paper_form.pdf new file mode 100644 index 000000000..961350f37 Binary files /dev/null and b/public/files/2022_23_sales_paper_form.pdf differ diff --git a/public/files/2023_24_lettings_paper_form.pdf b/public/files/2023_24_lettings_paper_form.pdf new file mode 100644 index 000000000..787e3b3b8 Binary files /dev/null and b/public/files/2023_24_lettings_paper_form.pdf differ diff --git a/public/files/2023_24_sales_paper_form.pdf b/public/files/2023_24_sales_paper_form.pdf new file mode 100644 index 000000000..327c90ab8 Binary files /dev/null and b/public/files/2023_24_sales_paper_form.pdf differ diff --git a/spec/components/bulk_upload_error_summary_table_component_spec.rb b/spec/components/bulk_upload_error_summary_table_component_spec.rb index a7468d70a..7fb7b65ed 100644 --- a/spec/components/bulk_upload_error_summary_table_component_spec.rb +++ b/spec/components/bulk_upload_error_summary_table_component_spec.rb @@ -3,7 +3,7 @@ require "rails_helper" RSpec.describe BulkUploadErrorSummaryTableComponent, type: :component do subject(:component) { described_class.new(bulk_upload:) } - let(:bulk_upload) { create(:bulk_upload) } + let(:bulk_upload) { create(:bulk_upload, :lettings) } before do stub_const("BulkUploadErrorSummaryTableComponent::DISPLAY_THRESHOLD", 0) @@ -52,7 +52,7 @@ RSpec.describe BulkUploadErrorSummaryTableComponent, type: :component do expect(row_1).to eql([ "A", "1", - BulkUpload::Lettings::Validator.question_for_field(error_1.field.to_sym), + bulk_upload.prefix_namespace::RowParser.question_for_field(error_1.field.to_sym), error_1.error, error_1.field, ]) @@ -62,7 +62,7 @@ RSpec.describe BulkUploadErrorSummaryTableComponent, type: :component do expect(row_2).to eql([ "B", "1", - BulkUpload::Lettings::Validator.question_for_field(error_2.field.to_sym), + bulk_upload.prefix_namespace::RowParser.question_for_field(error_2.field.to_sym), error_2.error, error_2.field, ]) @@ -89,7 +89,7 @@ RSpec.describe BulkUploadErrorSummaryTableComponent, type: :component do expect(row_1).to eql([ "A", "2", - BulkUpload::Lettings::Validator.question_for_field(error_1.field.to_sym), + bulk_upload.prefix_namespace::RowParser.question_for_field(error_1.field.to_sym), error_1.error, error_1.field, ]) diff --git a/spec/features/bulk_upload_sales_logs_spec.rb b/spec/features/bulk_upload_sales_logs_spec.rb index b69b6e237..e92b98a68 100644 --- a/spec/features/bulk_upload_sales_logs_spec.rb +++ b/spec/features/bulk_upload_sales_logs_spec.rb @@ -75,7 +75,7 @@ RSpec.describe "Bulk upload sales log" do expect(page).to have_link("Upload sales logs in bulk") click_link("Upload sales logs in bulk") - expect(page).to have_content("Upload sales logs in bulk (2022/23)") + expect(page).to have_content("Upload sales logs in bulk (2023/24)") click_button("Continue") expect(page).to have_content("Upload your file") diff --git a/spec/features/form/accessible_autocomplete_spec.rb b/spec/features/form/accessible_autocomplete_spec.rb index fd8102dcc..160d32d38 100644 --- a/spec/features/form/accessible_autocomplete_spec.rb +++ b/spec/features/form/accessible_autocomplete_spec.rb @@ -1,7 +1,7 @@ require "rails_helper" require_relative "helpers" -RSpec.describe "Accessible Automcomplete" do +RSpec.describe "Accessible Autocomplete" do include Helpers let(:user) { FactoryBot.create(:user) } let(:lettings_log) do diff --git a/spec/features/form/conditional_questions_spec.rb b/spec/features/form/conditional_questions_spec.rb index 4fc9387da..52d8886e0 100644 --- a/spec/features/form/conditional_questions_spec.rb +++ b/spec/features/form/conditional_questions_spec.rb @@ -54,13 +54,13 @@ RSpec.describe "Form Conditional Questions" do end it "gets cleared if the conditional question is hidden after editing the answer" do - sales_log.update!(national: 12, othernational: "other") - visit("/sales-logs/#{sales_log.id}/buyer-1-nationality") - expect(page).to have_field("sales-log-othernational-field", with: "other") + sales_log.update!(age1_known: 0, age1: 50) + visit("/sales-logs/#{sales_log.id}/buyer-1-age") + expect(page).to have_field("sales-log-age1-field", with: 50) - choose("sales-log-national-18-field", allow_label_click: true) - choose("sales-log-national-12-field", allow_label_click: true) - expect(page).to have_field("sales-log-othernational-field", with: "") + choose("sales-log-age1-known-1-field", allow_label_click: true) + choose("sales-log-age1-known-0-field", allow_label_click: true) + expect(page).to have_field("sales-log-age1-field", with: "") end end end diff --git a/spec/features/form/progressive_total_field_spec.rb b/spec/features/form/progressive_total_field_spec.rb index e5959d16d..787174c20 100644 --- a/spec/features/form/progressive_total_field_spec.rb +++ b/spec/features/form/progressive_total_field_spec.rb @@ -1,7 +1,7 @@ require "rails_helper" require_relative "helpers" -RSpec.describe "Accessible Automcomplete" do +RSpec.describe "Accessible Autocomplete" do include Helpers let(:user) { FactoryBot.create(:user) } let(:lettings_log) do diff --git a/spec/fixtures/files/lettings_logs_download_codes_only.csv b/spec/fixtures/files/lettings_logs_download_codes_only.csv new file mode 100644 index 000000000..b0bd8be1a --- /dev/null +++ b/spec/fixtures/files/lettings_logs_download_codes_only.csv @@ -0,0 +1,2 @@ +id,status,created_at,updated_at,created_by_name,is_dpo,owning_organisation_name,managing_organisation_name,collection_start_year,needstype,renewal,startdate,rent_type_detail,irproduct_other,tenancycode,propcode,age1,sex1,ecstat1,hhmemb,relat2,age2,sex2,retirement_value_check,ecstat2,armedforces,leftreg,illness,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_h,is_previous_la_inferred,prevloc_label,prevloc,illness_type_1,illness_type_2,is_la_inferred,la_label,la,postcode_known,postcode_full,previous_la_known,wchair,preg_occ,cbl,earnings,incfreq,net_income_value_check,benefits,hb,period,brent,scharge,pscharge,supcharg,tcharge,offered,layear,ppostcode_full,mrcdate,declaration,ethnic,national,prevten,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,reservist,startertenancy,tenancylength,tenancy,rsnvac,unittype_gn,beds,waityear,reasonpref,chr,cap,reasonother,housingneeds_f,housingneeds_g,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,property_owner_organisation,property_manager_organisation,purchaser_code,reason,majorrepairs,hbrentshortfall,property_relet,incref,first_time_property_let_as_social_housing,unitletas,builtype,voiddate,renttype,lettype,totchild,totelder,totadult,net_income_known,nocharge,is_carehome,household_charge,referral,tshortfall,chcharge,ppcodenk,age1_known,age2_known,age3_known,age4_known,age5_known,age6_known,age7_known,age8_known,ethnic_group,letting_allocation_unknown,details_known_2,details_known_3,details_known_4,details_known_5,details_known_6,details_known_7,details_known_8,has_benefits,wrent,wscharge,wpschrge,wsupchrg,wtcharge,wtshortfall,refused,housingneeds,wchchrg,newprop,relat3,relat4,relat5,relat6,relat7,relat8,rent_value_check,old_form_id,lar,irproduct,old_id,joint,tshortfall_known,sheltered,pregnancy_value_check,hhtype,new_old,vacdays,major_repairs_date_value_check,void_date_value_check,housingneeds_type,housingneeds_other,unresolved,updated_by_id,unittype_sh,scheme_code,scheme_service_name,scheme_sensitive,scheme_type,scheme_registered_under_care_act,scheme_owning_organisation_name,scheme_primary_client_group,scheme_has_other_client_group,scheme_secondary_client_group,scheme_support_type,scheme_intended_stay,scheme_created_at,location_code,location_postcode,location_name,location_units,location_type_of_unit,location_mobility_type,location_admin_district,location_startdate +{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,false,DLUHC,DLUHC,2021,2,,2 October 2021,2,,,,,,,,,,,,,,,,,,,,false,,,,,false,Westminster,E09000033,,SE1 1TE,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,2,8,0,0,0,,0,,,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,0,,,,,,,,,,,,,,,,,,,9,1,,,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},0,1,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2021-04-01 00:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,6,A,Westminster,{location_startdate} diff --git a/spec/helpers/tasklist_helper_spec.rb b/spec/helpers/tasklist_helper_spec.rb index b5bfe89f5..f0cec71ad 100644 --- a/spec/helpers/tasklist_helper_spec.rb +++ b/spec/helpers/tasklist_helper_spec.rb @@ -9,7 +9,6 @@ RSpec.describe TasklistHelper do example.run end Timecop.return - Singleton.__init__(FormHandler) end describe "with lettings" do diff --git a/spec/mailers/bulk_upload_mailer_spec.rb b/spec/mailers/bulk_upload_mailer_spec.rb index 0e38617a3..323b49b8f 100644 --- a/spec/mailers/bulk_upload_mailer_spec.rb +++ b/spec/mailers/bulk_upload_mailer_spec.rb @@ -72,11 +72,12 @@ RSpec.describe BulkUploadMailer do upload_timestamp: bulk_upload.created_at, lettings_or_sales: bulk_upload.log_type, year_combo: bulk_upload.year_combo, + errors: "- foo\n- bar", bulk_upload_link: start_bulk_upload_lettings_logs_url, }, ) - mailer.send_bulk_upload_failed_service_error_mail(bulk_upload:) + mailer.send_bulk_upload_failed_service_error_mail(bulk_upload:, errors: %w[foo bar]) end end diff --git a/spec/models/form/lettings/pages/starter_tenancy_type_spec.rb b/spec/models/form/lettings/pages/starter_tenancy_type_spec.rb new file mode 100644 index 000000000..795dee561 --- /dev/null +++ b/spec/models/form/lettings/pages/starter_tenancy_type_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Pages::StarterTenancyType, type: :model do + subject(:page) { described_class.new(nil, nil, subsection) } + + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[tenancy tenancyother]) + end + + it "has the correct id" do + expect(page.id).to eq("starter_tenancy_type") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has the correct depends_on" do + expect(page.depends_on).to eq([{ "starter_tenancy?" => true }]) + end +end diff --git a/spec/models/form/lettings/pages/tenancy_length_spec.rb b/spec/models/form/lettings/pages/tenancy_length_spec.rb new file mode 100644 index 000000000..d4ea920ae --- /dev/null +++ b/spec/models/form/lettings/pages/tenancy_length_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Pages::TenancyLength, type: :model do + subject(:page) { described_class.new(nil, nil, subsection) } + + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq subsection + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq %w[tenancylength] + end + + it "has the correct id" do + expect(page.id).to eq "tenancy_length" + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has the correct depends_on" do + expect(page.depends_on).to eq [{ "tenancy_type_fixed_term?" => true }] + end +end diff --git a/spec/models/form/lettings/pages/tenancy_type_spec.rb b/spec/models/form/lettings/pages/tenancy_type_spec.rb new file mode 100644 index 000000000..ef772e85e --- /dev/null +++ b/spec/models/form/lettings/pages/tenancy_type_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Pages::TenancyType, type: :model do + subject(:page) { described_class.new(nil, nil, subsection) } + + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq %w[tenancy tenancyother] + end + + it "has the correct id" do + expect(page.id).to eq "tenancy_type" + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has the correct depends_on" do + expect(page.depends_on).to eq [{ "starter_tenancy?" => false }] + end +end diff --git a/spec/models/form/lettings/questions/la_spec.rb b/spec/models/form/lettings/questions/la_spec.rb index 05ebb2d69..f73a29660 100644 --- a/spec/models/form/lettings/questions/la_spec.rb +++ b/spec/models/form/lettings/questions/la_spec.rb @@ -10,8 +10,6 @@ RSpec.describe Form::Lettings::Questions::La, type: :model do let(:start_date) { Time.utc(2023, 4, 1) } it "has the correct answer_options" do - Rails.application.load_seed - expect(question.answer_options).to eq({ "" => "Select an option", "E06000001" => "Hartlepool", diff --git a/spec/models/form/lettings/questions/nationality_spec.rb b/spec/models/form/lettings/questions/nationality_spec.rb new file mode 100644 index 000000000..d0eaeb69b --- /dev/null +++ b/spec/models/form/lettings/questions/nationality_spec.rb @@ -0,0 +1,48 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Questions::Nationality, 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("national") + end + + it "has the correct header" do + expect(question.header).to eq("What is the nationality of the lead tenant?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Lead tenant’s nationality") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "has the correct hint_text" do + expect(question.hint_text).to eq("The lead tenant is the person in the household who does the most paid work. If several people do the same paid work, the lead tenant is whoever is the oldest.") + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "18" => { "value" => "United Kingdom" }, + "17" => { "value" => "Republic of Ireland" }, + "19" => { "value" => "European Economic Area (EEA) country, excluding Ireland" }, + "20" => { "value" => "Afghanistan" }, + "21" => { "value" => "Ukraine" }, + "12" => { "value" => "Other" }, + "divider" => true, + "13" => { "value" => "Tenant prefers not to say" }, + }) + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end +end diff --git a/spec/models/form/lettings/questions/prevloc_spec.rb b/spec/models/form/lettings/questions/prevloc_spec.rb index fbecaaceb..7d2b141a2 100644 --- a/spec/models/form/lettings/questions/prevloc_spec.rb +++ b/spec/models/form/lettings/questions/prevloc_spec.rb @@ -10,8 +10,6 @@ RSpec.describe Form::Lettings::Questions::Prevloc, type: :model do let(:start_date) { Time.utc(2023, 4, 1) } it "has the correct answer_options" do - Rails.application.load_seed - expect(question.answer_options).to eq({ "" => "Select an option", "S12000033" => "Aberdeen City", diff --git a/spec/models/form/lettings/questions/reason_spec.rb b/spec/models/form/lettings/questions/reason_spec.rb new file mode 100644 index 000000000..24d28fd4b --- /dev/null +++ b/spec/models/form/lettings/questions/reason_spec.rb @@ -0,0 +1,158 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Questions::Reason, 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("reason") + end + + it "has the correct header" do + expect(question.header).to eq("What is the tenant’s main reason for the household leaving their last settled home?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Reason for leaving last settled home") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to eq(0) + end + + it "has the correct hint" do + expect(question.hint_text).to eq("The tenant’s ‘last settled home’ is their last long-standing home. For tenants who were in temporary accommodation or sleeping rough, their last settled home is where they were living previously.") + end + + it "has the correct conditional_for" do + expect(question.conditional_for).to eq({ "reasonother" => [20] }) + end + + it "is not marked as derived" do + expect(question).not_to be_derived + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "40" => { + "value" => "End of assured shorthold tenancy (no fault)", + }, + "41" => { + "value" => "End of assured shorthold tenancy (eviction or tenant at fault)", + }, + "42" => { + "value" => "End of fixed term tenancy (no fault)", + }, + "43" => { + "value" => "End of fixed term tenancy (eviction or tenant at fault)", + }, + "1" => { + "value" => "Permanently decanted from another property owned by this landlord", + }, + "46" => { + "value" => "Discharged from long-stay hospital or similar institution", + }, + "45" => { + "value" => "Discharged from prison", + }, + "2" => { + "value" => "Left home country as a refugee", + }, + "4" => { + "value" => "Loss of tied accommodation", + }, + "9" => { + "value" => "Asked to leave by family or friends", + }, + "44" => { + "value" => "Death of household member in last settled accommodation", + }, + "8" => { + "value" => "Relationship breakdown (non-violent) with partner", + }, + "16" => { + "value" => "To move nearer to family, friends or school", + }, + "17" => { + "value" => "To move nearer to work", + }, + "48" => { + "value" => "Domestic abuse - previously joint tenancy with partner", + }, + "49" => { + "value" => "Domestic abuse - other", + }, + "31" => { + "value" => "Hate crime", + }, + "10" => { + "value" => "Racial harassment", + }, + "11" => { + "value" => "Other problems with neighbours", + }, + "35" => { + "value" => "Couldn’t afford fees attached to renewing the tenancy", + }, + "36" => { + "value" => "Couldn’t afford increase in rent", + }, + "38" => { + "value" => "Couldn’t afford rent or mortgage (employment)", + }, + "37" => { + "value" => "Couldn’t afford rent or mortgage (welfare reforms)", + }, + "39" => { + "value" => "Couldn’t afford rent or mortgage (other)", + }, + "34" => { + "value" => "Repossession", + }, + "12" => { + "value" => "Property unsuitable because of overcrowding", + }, + "13" => { + "value" => "Property unsuitable because of ill health or disability", + }, + "14" => { + "value" => "Property unsuitable because of poor condition", + }, + "18" => { + "value" => "To move to accommodation with support", + }, + "19" => { + "value" => "To move to independent accommodation", + }, + "30" => { + "value" => "Under occupation (no incentive)", + }, + "29" => { + "value" => "Under occupation (offered incentive to downsize)", + }, + "20" => { + "value" => "Other", + }, + "47" => { + "value" => "Tenant prefers not to say", + }, + "divider" => { + "value" => true, + }, + "28" => { + "value" => "Don’t know", + }, + }) + end +end diff --git a/spec/models/form/lettings/questions/tenancy_length_spec.rb b/spec/models/form/lettings/questions/tenancy_length_spec.rb new file mode 100644 index 000000000..8eea52a81 --- /dev/null +++ b/spec/models/form/lettings/questions/tenancy_length_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Questions::TenancyLength, 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("tenancylength") + end + + it "has the correct header" do + expect(question.header).to eq("What is the length of the fixed-term tenancy to the nearest year?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Length of fixed-term tenancy") + end + + it "has the correct type" do + expect(question.type).to eq("numeric") + end + + it "has the correct hint_text" do + expect(question.hint_text).to eq("Don’t include the starter or introductory period.") + end + + it "has the correct minimum and maximum" 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 +end diff --git a/spec/models/form/lettings/questions/tenancy_other_spec.rb b/spec/models/form/lettings/questions/tenancy_other_spec.rb new file mode 100644 index 000000000..58a656e57 --- /dev/null +++ b/spec/models/form/lettings/questions/tenancy_other_spec.rb @@ -0,0 +1,35 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Questions::TenancyOther, 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("tenancyother") + end + + it "has the correct header" do + expect(question.header).to eq("Please state the tenancy type") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("") + end + + it "has the correct type" do + expect(question.type).to eq("text") + end + + it "has the correct hint_text" do + expect(question.hint_text).to eq("") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end +end diff --git a/spec/models/form/lettings/questions/tenancy_type_spec.rb b/spec/models/form/lettings/questions/tenancy_type_spec.rb new file mode 100644 index 000000000..8fd8a50f0 --- /dev/null +++ b/spec/models/form/lettings/questions/tenancy_type_spec.rb @@ -0,0 +1,65 @@ +require "rails_helper" + +RSpec.describe Form::Lettings::Questions::TenancyType, 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("tenancy") + end + + it "has the correct header" do + expect(question.header).to eq("What is the type of tenancy?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Type of main tenancy") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "has the correct hint_text" do + expect(question.hint_text).to eq("") + end + + it "has the correct conditional_for" do + expect(question.conditional_for).to eq({ "tenancyother" => [3] }) + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "4" => { + "value" => "Assured Shorthold Tenancy (AST) – Fixed term", + "hint" => "Mostly housing associations provide these. Fixed term tenancies are intended to be for a set amount of time up to 20 years.", + }, + "6" => { + "value" => "Secure – fixed term", + "hint" => "Mostly local authorities provide these. Fixed term tenancies are intended to be for a set amount of time up to 20 years.", + }, + "2" => { + "value" => "Assured – lifetime", + }, + "7" => { + "value" => "Secure – lifetime", + }, + "5" => { + "value" => "Licence agreement", + "hint" => "Licence agreements are mostly used for Supported Housing and work on a rolling basis.", + }, + "3" => { + "value" => "Other", + }, + }) + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end +end diff --git a/spec/models/form/sales/pages/buyer2_live_in_property_spec.rb b/spec/models/form/sales/pages/buyer2_live_in_property_spec.rb index 258e37462..555e78927 100644 --- a/spec/models/form/sales/pages/buyer2_live_in_property_spec.rb +++ b/spec/models/form/sales/pages/buyer2_live_in_property_spec.rb @@ -30,12 +30,12 @@ RSpec.describe Form::Sales::Pages::Buyer2LiveInProperty, type: :model do it "has correct depends_on" do expect(page.depends_on).to eq([ { - "jointpur" => 1, + "joint_purchase?" => true, "privacynotice" => 1, }, { - "jointpur" => 1, - "noint" => 1, + "joint_purchase?" => true, + "buyer_not_interviewed?" => true, }, ]) end diff --git a/spec/models/form/sales/pages/buyer2_living_in_spec.rb b/spec/models/form/sales/pages/buyer2_living_in_spec.rb new file mode 100644 index 000000000..9bfef1ff4 --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_living_in_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2LivingIn, type: :model do + subject(:page) { described_class.new(nil, nil, subsection) } + + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[buy2living]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_living_in") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ "buyer_two_will_live_in_property?" => true }]) + end +end diff --git a/spec/models/form/sales/pages/buyer2_nationality_spec.rb b/spec/models/form/sales/pages/buyer2_nationality_spec.rb new file mode 100644 index 000000000..92c891f88 --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_nationality_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2Nationality, type: :model do + subject(:page) { described_class.new(nil, nil, subsection) } + + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to be subsection + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq %w[nationalbuy2] + end + + it "has the correct id" do + expect(page.id).to eq "buyer_2_nationality" + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq [{ "joint_purchase?" => true }] + end +end diff --git a/spec/models/form/sales/pages/buyer2_previous_housing_situation_spec.rb b/spec/models/form/sales/pages/buyer2_previous_housing_situation_spec.rb new file mode 100644 index 000000000..ac5611a8f --- /dev/null +++ b/spec/models/form/sales/pages/buyer2_previous_housing_situation_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Pages::Buyer2PreviousHousingSituation, type: :model do + subject(:page) { described_class.new(nil, nil, subsection) } + + let(:subsection) { instance_double(Form::Subsection) } + + it "has correct subsection" do + expect(page.subsection).to eq(subsection) + end + + it "has correct questions" do + expect(page.questions.map(&:id)).to eq(%w[prevtenbuy2]) + end + + it "has the correct id" do + expect(page.id).to eq("buyer_2_previous_housing_situation") + end + + it "has the correct header" do + expect(page.header).to be_nil + end + + it "has the correct description" do + expect(page.description).to be_nil + end + + it "has correct depends_on" do + expect(page.depends_on).to eq([{ "buyer_two_not_already_living_in_property?" => true }]) + end +end diff --git a/spec/models/form/sales/pages/nationality1_spec.rb b/spec/models/form/sales/pages/nationality1_spec.rb index da5a5f802..37b58a89f 100644 --- a/spec/models/form/sales/pages/nationality1_spec.rb +++ b/spec/models/form/sales/pages/nationality1_spec.rb @@ -1,22 +1,20 @@ require "rails_helper" RSpec.describe Form::Sales::Pages::Nationality1, type: :model do - subject(:page) { described_class.new(page_id, page_definition, subsection) } + subject(:page) { described_class.new(nil, nil, subsection) } - let(:page_id) { nil } - let(:page_definition) { nil } let(:subsection) { instance_double(Form::Subsection) } it "has correct subsection" do - expect(page.subsection).to eq(subsection) + expect(page.subsection).to be subsection end it "has correct questions" do - expect(page.questions.map(&:id)).to eq(%w[national othernational]) + expect(page.questions.map(&:id)).to eq %w[national] end it "has the correct id" do - expect(page.id).to eq("buyer_1_nationality") + expect(page.id).to eq "buyer_1_nationality" end it "has the correct header" do @@ -28,6 +26,6 @@ RSpec.describe Form::Sales::Pages::Nationality1, type: :model do end it "has correct depends_on" do - expect(page.depends_on).to eq([{ "privacynotice" => 1 }, { "noint" => 1 }]) + expect(page.depends_on).to eq [{ "privacynotice" => 1 }, { "buyer_not_interviewed?" => true }] end end diff --git a/spec/models/form/sales/questions/buyer1_previous_tenure_spec.rb b/spec/models/form/sales/questions/buyer1_previous_tenure_spec.rb index 4321700b4..f99b1c01f 100644 --- a/spec/models/form/sales/questions/buyer1_previous_tenure_spec.rb +++ b/spec/models/form/sales/questions/buyer1_previous_tenure_spec.rb @@ -32,13 +32,13 @@ RSpec.describe Form::Sales::Questions::Buyer1PreviousTenure, type: :model do it "has the correct answer_options" do expect(question.answer_options).to eq({ - "1" => { "value" => "Local Authority" }, + "1" => { "value" => "Local authority tenant" }, "2" => { "value" => "Private registered provider or housing association tenant" }, "3" => { "value" => "Private tenant" }, "4" => { "value" => "Tied home or renting with job" }, "5" => { "value" => "Owner occupier" }, "6" => { "value" => "Living with family or friends" }, - "7" => { "value" => "Temporary accomodation" }, + "7" => { "value" => "Temporary accommodation" }, "9" => { "value" => "Other" }, "0" => { "value" => "Don’t know" }, }) diff --git a/spec/models/form/sales/questions/buyer2_living_in_spec.rb b/spec/models/form/sales/questions/buyer2_living_in_spec.rb new file mode 100644 index 000000000..4fca65f3f --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_living_in_spec.rb @@ -0,0 +1,43 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2LivingIn, 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 "buy2living" + end + + it "has the correct header" do + expect(question.header).to eq "At the time of purchase, was buyer 2 living at the same address as buyer 1?" + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq "Buyer 2 living at the same address" + end + + it "has the correct type" do + expect(question.type).to eq "radio" + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint_text" do + expect(question.hint_text).to eq "" + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "1" => { "value" => "Yes" }, + "2" => { "value" => "No" }, + "3" => { "value" => "Don't know" }, + }) + end +end diff --git a/spec/models/form/sales/questions/buyer2_nationality_spec.rb b/spec/models/form/sales/questions/buyer2_nationality_spec.rb new file mode 100644 index 000000000..ed1f96c28 --- /dev/null +++ b/spec/models/form/sales/questions/buyer2_nationality_spec.rb @@ -0,0 +1,63 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::Buyer2Nationality, 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 be page + end + + it "has the correct id" do + expect(question.id).to eq "nationalbuy2" + end + + it "has the correct header" do + expect(question.header).to eq "What is buyer 2’s nationality?" + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq "Buyer 2’s nationality" + end + + it "has the correct type" do + expect(question.type).to eq "radio" + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint" do + expect(question.hint_text).to be nil + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "18" => { "value" => "United Kingdom" }, + "17" => { "value" => "Republic of Ireland" }, + "19" => { "value" => "European Economic Area (EEA), excluding ROI" }, + "12" => { "value" => "Other" }, + "13" => { "value" => "Buyer prefers not to say" }, + }) + end + + it "has correct conditional for" do + expect(question.conditional_for).to be_nil + end + + it "has correct hidden in check answers" do + expect(question.hidden_in_check_answers).to be_nil + end + + it "has the correct check_answers_card_number" do + expect(question.check_answers_card_number).to be 2 + end + + it "has the correct inferred_check_answers_value" do + expect(question.inferred_check_answers_value).to eq([ + { "condition" => { "national" => 13 }, "value" => "Prefers not to say" }, + ]) + end +end diff --git a/spec/models/form/sales/questions/nationality1_spec.rb b/spec/models/form/sales/questions/nationality1_spec.rb index 18f659e86..4b8816dbf 100644 --- a/spec/models/form/sales/questions/nationality1_spec.rb +++ b/spec/models/form/sales/questions/nationality1_spec.rb @@ -1,30 +1,28 @@ require "rails_helper" RSpec.describe Form::Sales::Questions::Nationality1, type: :model do - subject(:question) { described_class.new(question_id, question_definition, page) } + subject(:question) { described_class.new(nil, nil, 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) + expect(question.page).to be page end it "has the correct id" do - expect(question.id).to eq("national") + expect(question.id).to eq "national" end it "has the correct header" do - expect(question.header).to eq("What is buyer 1’s nationality?") + expect(question.header).to eq "What is buyer 1’s nationality?" end it "has the correct check_answer_label" do - expect(question.check_answer_label).to eq("Buyer 1’s nationality") + expect(question.check_answer_label).to eq "Buyer 1’s nationality" end it "has the correct type" do - expect(question.type).to eq("radio") + expect(question.type).to eq "radio" end it "is not marked as derived" do @@ -32,7 +30,7 @@ RSpec.describe Form::Sales::Questions::Nationality1, type: :model do end it "has the correct hint" do - expect(question.hint_text).to eq("Buyer 1 is the person in the household who does the most paid work. If it’s a joint purchase and the buyers do the same amount of paid work, buyer 1 is whoever is the oldest.") + expect(question.hint_text).to eq "Buyer 1 is the person in the household who does the most paid work. If it’s a joint purchase and the buyers do the same amount of paid work, buyer 1 is whoever is the oldest." end it "has the correct answer_options" do @@ -46,23 +44,15 @@ RSpec.describe Form::Sales::Questions::Nationality1, type: :model do end it "has correct conditional for" do - expect(question.conditional_for).to eq({ - "othernational" => [12], - }) + expect(question.conditional_for).to be_nil end it "has correct hidden in check answers" do - expect(question.hidden_in_check_answers).to eq({ - "depends_on" => [ - { - "national" => 12, - }, - ], - }) + expect(question.hidden_in_check_answers).to be_nil end it "has the correct check_answers_card_number" do - expect(question.check_answers_card_number).to eq(1) + expect(question.check_answers_card_number).to be 1 end it "has the correct inferred_check_answers_value" do diff --git a/spec/models/form/sales/questions/other_nationality1_spec.rb b/spec/models/form/sales/questions/other_nationality1_spec.rb deleted file mode 100644 index c3faa73dc..000000000 --- a/spec/models/form/sales/questions/other_nationality1_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require "rails_helper" - -RSpec.describe Form::Sales::Questions::OtherNationality1, 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("othernational") - end - - it "has the correct header" do - expect(question.header).to eq("Nationality") - end - - it "has the correct check_answer_label" do - expect(question.check_answer_label).to eq("Buyer 1’s nationality") - end - - it "has the correct type" do - expect(question.type).to eq("text") - end - - it "is not marked as derived" do - expect(question.derived?).to be false - end - - it "has the correct hint" do - expect(question.hint_text).to be_nil - end - - it "has the correct check_answers_card_number" do - expect(question.check_answers_card_number).to eq(1) - end -end diff --git a/spec/models/form/sales/questions/previous_tenure_buyer2_spec.rb b/spec/models/form/sales/questions/previous_tenure_buyer2_spec.rb new file mode 100644 index 000000000..713d4b3e8 --- /dev/null +++ b/spec/models/form/sales/questions/previous_tenure_buyer2_spec.rb @@ -0,0 +1,49 @@ +require "rails_helper" + +RSpec.describe Form::Sales::Questions::PreviousTenureBuyer2, 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("prevtenbuy2") + end + + it "has the correct header" do + expect(question.header).to eq("What was buyer 2’s previous tenure?") + end + + it "has the correct check_answer_label" do + expect(question.check_answer_label).to eq("Buyer 2’s previous tenure") + end + + it "has the correct type" do + expect(question.type).to eq("radio") + end + + it "is not marked as derived" do + expect(question.derived?).to be false + end + + it "has the correct hint_text" do + expect(question.hint_text).to eq("") + end + + it "has the correct answer_options" do + expect(question.answer_options).to eq({ + "1" => { "value" => "Local authority tenant" }, + "2" => { "value" => "Private registered provider or housing association tenant" }, + "3" => { "value" => "Private tenant" }, + "5" => { "value" => "Owner occupier" }, + "4" => { "value" => "Tied home or renting with job" }, + "6" => { "value" => "Living with family or friends" }, + "7" => { "value" => "Temporary accommodation" }, + "9" => { "value" => "Other" }, + "0" => { "value" => "Don't know" }, + }) + end +end diff --git a/spec/models/form/sales/questions/prevloc_spec.rb b/spec/models/form/sales/questions/prevloc_spec.rb index e4316af47..e27b295be 100644 --- a/spec/models/form/sales/questions/prevloc_spec.rb +++ b/spec/models/form/sales/questions/prevloc_spec.rb @@ -37,8 +37,6 @@ RSpec.describe Form::Sales::Questions::Prevloc, type: :model do let(:start_date) { Time.utc(2022, 4, 1) } it "has the correct answer_options" do - Rails.application.load_seed - expect(question.answer_options).to eq({ "" => "Select an option", "S12000033" => "Aberdeen City", @@ -432,8 +430,6 @@ RSpec.describe Form::Sales::Questions::Prevloc, type: :model do let(:start_date) { Time.utc(2023, 4, 1) } it "has the correct answer_options" do - Rails.application.load_seed - expect(question.answer_options).to eq({ "" => "Select an option", "S12000033" => "Aberdeen City", diff --git a/spec/models/form/sales/questions/property_local_authority_spec.rb b/spec/models/form/sales/questions/property_local_authority_spec.rb index a5ffe8870..5b4d1cb1f 100644 --- a/spec/models/form/sales/questions/property_local_authority_spec.rb +++ b/spec/models/form/sales/questions/property_local_authority_spec.rb @@ -37,8 +37,6 @@ RSpec.describe Form::Sales::Questions::PropertyLocalAuthority, type: :model do let(:start_date) { Time.utc(2022, 4, 1) } it "has the correct answer_options" do - Rails.application.load_seed - expect(question.answer_options).to eq({ "" => "Select an option", "E06000001" => "Hartlepool", @@ -363,8 +361,6 @@ RSpec.describe Form::Sales::Questions::PropertyLocalAuthority, type: :model do let(:start_date) { Time.utc(2023, 4, 1) } it "has the correct answer_options" do - Rails.application.load_seed - expect(question.answer_options).to eq({ "" => "Select an option", "E06000001" => "Hartlepool", diff --git a/spec/models/form/sales/subsections/household_characteristics_spec.rb b/spec/models/form/sales/subsections/household_characteristics_spec.rb index 5a025e8de..b47ac970d 100644 --- a/spec/models/form/sales/subsections/household_characteristics_spec.rb +++ b/spec/models/form/sales/subsections/household_characteristics_spec.rb @@ -135,6 +135,7 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model buyer_2_ethnic_background_arab buyer_2_ethnic_background_mixed buyer_2_ethnic_background_white + buyer_2_nationality buyer_2_working_situation working_situation_2_retirement_value_check_joint_purchase working_situation_buyer_2_income_value_check diff --git a/spec/models/form/sales/subsections/household_situation_spec.rb b/spec/models/form/sales/subsections/household_situation_spec.rb index e88dda99f..10898b27c 100644 --- a/spec/models/form/sales/subsections/household_situation_spec.rb +++ b/spec/models/form/sales/subsections/household_situation_spec.rb @@ -1,25 +1,44 @@ require "rails_helper" RSpec.describe Form::Sales::Subsections::HouseholdSituation, type: :model do - subject(:household_characteristics) { described_class.new(subsection_id, subsection_definition, section) } + subject(:household_characteristics) { described_class.new(nil, nil, section) } - let(:subsection_id) { nil } - let(:subsection_definition) { nil } - let(:section) { instance_double(Form::Sales::Sections::Household) } + let(:start_date) { Time.utc(2023, 4, 1) } + let(:form) { instance_double(Form, start_date:) } + let(:section) { instance_double(Form::Sales::Sections::Household, form:) } it "has correct section" do expect(household_characteristics.section).to eq(section) end - it "has correct pages" do - expect(household_characteristics.pages.map(&:id)).to eq( - %w[ - buyer1_previous_tenure - last_accommodation - last_accommodation_la - buyers_organisations - ], - ) + context "when the log belongs to the 22/23 collection" do + let(:start_date) { Time.utc(2022, 4, 1) } + + it "has correct pages" do + expect(household_characteristics.pages.map(&:id)).to eq( + %w[ + buyer1_previous_tenure + last_accommodation + last_accommodation_la + buyers_organisations + ], + ) + end + end + + context "when the log belongs to the 23/24 collection" do + it "has correct pages" do + expect(household_characteristics.pages.map(&:id)).to eq( + %w[ + buyer1_previous_tenure + last_accommodation + last_accommodation_la + buyers_organisations + buyer_2_living_in + buyer_2_previous_housing_situation + ], + ) + end end it "has the correct id" do diff --git a/spec/models/forms/bulk_upload_lettings/year_spec.rb b/spec/models/forms/bulk_upload_lettings/year_spec.rb index 0b0babb30..ebb65e517 100644 --- a/spec/models/forms/bulk_upload_lettings/year_spec.rb +++ b/spec/models/forms/bulk_upload_lettings/year_spec.rb @@ -5,8 +5,8 @@ RSpec.describe Forms::BulkUploadLettings::Year do describe "#options" do it "returns correct years" do - expect(form.options.map(&:id)).to eql([2022, 2021]) - expect(form.options.map(&:name)).to eql(%w[2022/2023 2021/2022]) + expect(form.options.map(&:id)).to eql([2023, 2022]) + expect(form.options.map(&:name)).to eql(%w[2023/2024 2022/2023]) end end end diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index c1bfafb57..c6f559aec 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -2685,40 +2685,67 @@ RSpec.describe LettingsLog do let(:user) { FactoryBot.create(:user, organisation: location.scheme.owning_organisation) } let(:expected_content) { csv_export_file.read } - before do - Timecop.freeze(Time.utc(2022, 6, 5)) - lettings_log = FactoryBot.create(:lettings_log, needstype: 2, scheme:, location:, owning_organisation: scheme.owning_organisation, created_by: user, rent_type: 2, startdate: Time.zone.local(2021, 10, 2)) - expected_content.sub!(/\{id\}/, lettings_log["id"].to_s) - expected_content.sub!(/\{scheme_code\}/, "S#{scheme['id']}") - expected_content.sub!(/\{scheme_service_name\}/, scheme["service_name"].to_s) - expected_content.sub!(/\{scheme_sensitive\}/, scheme["sensitive"].to_s) - expected_content.sub!(/\{scheme_primary_client_group\}/, scheme["primary_client_group"].to_s) - expected_content.sub!(/\{scheme_secondary_client_group\}/, scheme["secondary_client_group"].to_s) - expected_content.sub!(/\{scheme_support_type\}/, scheme["support_type"].to_s) - expected_content.sub!(/\{scheme_intended_stay\}/, scheme["intended_stay"].to_s) - expected_content.sub!(/\{location_code\}/, location["id"].to_s) - expected_content.sub!(/\{location_startdate\}/, location["startdate"].to_s) - expected_content.sub!(/\{scheme_id\}/, scheme["service_name"].to_s) - expected_content.sub!(/\{location_id\}/, location["id"].to_s) - end - after do Timecop.unfreeze end - context "with a support user" do - let(:csv_export_file) { File.open("spec/fixtures/files/lettings_logs_download.csv", "r:UTF-8") } + context "with values represented as human readable labels" do + before do + Timecop.freeze(Time.utc(2022, 6, 5)) + lettings_log = FactoryBot.create(:lettings_log, needstype: 2, scheme:, location:, owning_organisation: scheme.owning_organisation, created_by: user, rent_type: 2, startdate: Time.zone.local(2021, 10, 2)) + expected_content.sub!(/\{id\}/, lettings_log["id"].to_s) + expected_content.sub!(/\{scheme_code\}/, "S#{scheme['id']}") + expected_content.sub!(/\{scheme_service_name\}/, scheme["service_name"].to_s) + expected_content.sub!(/\{scheme_sensitive\}/, scheme["sensitive"].to_s) + expected_content.sub!(/\{scheme_primary_client_group\}/, scheme["primary_client_group"].to_s) + expected_content.sub!(/\{scheme_secondary_client_group\}/, scheme["secondary_client_group"].to_s) + expected_content.sub!(/\{scheme_support_type\}/, scheme["support_type"].to_s) + expected_content.sub!(/\{scheme_intended_stay\}/, scheme["intended_stay"].to_s) + expected_content.sub!(/\{location_code\}/, location["id"].to_s) + expected_content.sub!(/\{location_startdate\}/, location["startdate"].to_s) + expected_content.sub!(/\{scheme_id\}/, scheme["service_name"].to_s) + expected_content.sub!(/\{location_id\}/, location["id"].to_s) + end - it "generates a correct csv from a lettings log" do - expect(described_class.to_csv).to eq(expected_content) + context "with a support user" do + let(:csv_export_file) { File.open("spec/fixtures/files/lettings_logs_download.csv", "r:UTF-8") } + + it "generates a correct csv from a lettings log" do + expect(described_class.to_csv(codes_only_export: false)).to eq(expected_content) + end + end + + context "with a non support user" do + let(:csv_export_file) { File.open("spec/fixtures/files/lettings_logs_download_non_support.csv", "r:UTF-8") } + + it "generates a correct csv from a lettings log" do + expect(described_class.to_csv(user, codes_only_export: false)).to eq(expected_content) + end end end - context "with a non support user" do - let(:csv_export_file) { File.open("spec/fixtures/files/lettings_logs_download_non_support.csv", "r:UTF-8") } + context "with values represented as codes" do + before do + Timecop.freeze(Time.utc(2022, 6, 5)) + lettings_log = FactoryBot.create(:lettings_log, needstype: 2, scheme:, location:, owning_organisation: scheme.owning_organisation, created_by: user, rent_type: 2, startdate: Time.zone.local(2021, 10, 2)) + expected_content.sub!(/\{id\}/, lettings_log["id"].to_s) + expected_content.sub!(/\{scheme_code\}/, "S#{scheme.id}") + expected_content.sub!(/\{scheme_service_name\}/, scheme.service_name.to_s) + expected_content.sub!(/\{scheme_sensitive\}/, scheme.sensitive_before_type_cast.to_s) + expected_content.sub!(/\{scheme_primary_client_group\}/, scheme.primary_client_group_before_type_cast.to_s) + expected_content.sub!(/\{scheme_secondary_client_group\}/, scheme.secondary_client_group_before_type_cast.to_s) + expected_content.sub!(/\{scheme_support_type\}/, scheme.support_type_before_type_cast.to_s) + expected_content.sub!(/\{scheme_intended_stay\}/, scheme.intended_stay_before_type_cast.to_s) + expected_content.sub!(/\{location_code\}/, location.id.to_s) + expected_content.sub!(/\{location_startdate\}/, location.startdate.to_s) + expected_content.sub!(/\{scheme_id\}/, scheme.service_name.to_s) + expected_content.sub!(/\{location_id\}/, location.id.to_s) + end + + let(:csv_export_file) { File.open("spec/fixtures/files/lettings_logs_download_codes_only.csv", "r:UTF-8") } it "generates a correct csv from a lettings log" do - expect(described_class.to_csv(user)).to eq(expected_content) + expect(described_class.to_csv(codes_only_export: true)).to eq(expected_content) end end end diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index 9a5874415..8d1284fa5 100644 --- a/spec/models/location_spec.rb +++ b/spec/models/location_spec.rb @@ -1,10 +1,6 @@ require "rails_helper" RSpec.describe Location, type: :model do - before do - Rails.application.load_seed - end - describe "#new" do let(:location) { FactoryBot.build(:location) } diff --git a/spec/models/sales_log_spec.rb b/spec/models/sales_log_spec.rb index afa8c451b..e8798b145 100644 --- a/spec/models/sales_log_spec.rb +++ b/spec/models/sales_log_spec.rb @@ -247,7 +247,6 @@ RSpec.describe SalesLog, type: :model do after do Timecop.unfreeze - Singleton.__init__(FormHandler) end it "correctly sets la as nil" do @@ -278,7 +277,6 @@ RSpec.describe SalesLog, type: :model do after do Timecop.unfreeze - Singleton.__init__(FormHandler) end it "correctly infers new la" do diff --git a/spec/models/validations/sales/financial_validations_spec.rb b/spec/models/validations/sales/financial_validations_spec.rb index fae0e4536..d9406aec9 100644 --- a/spec/models/validations/sales/financial_validations_spec.rb +++ b/spec/models/validations/sales/financial_validations_spec.rb @@ -160,6 +160,48 @@ RSpec.describe Validations::Sales::FinancialValidations do end end + describe "#validate_percentage_bought_at_least_threshold" do + let(:record) { FactoryBot.create(:sales_log) } + + it "adds an error to stairbought and type if the percentage bought is less than the threshold (which is 1% by default, but higher for some shared ownership types)" do + record.stairbought = 9 + [2, 16, 18, 24].each do |type| + record.type = type + financial_validator.validate_percentage_bought_at_least_threshold(record) + expect(record.errors["stairbought"]).to eq(["The minimum increase in equity while staircasing is 10%"]) + expect(record.errors["type"]).to eq(["The minimum increase in equity while staircasing is 10% for this shared ownership type"]) + record.errors.clear + end + + record.stairbought = 0 + [28, 30, 31, 32].each do |type| + record.type = type + financial_validator.validate_percentage_bought_at_least_threshold(record) + expect(record.errors["stairbought"]).to eq(["The minimum increase in equity while staircasing is 1%"]) + expect(record.errors["type"]).to eq(["The minimum increase in equity while staircasing is 1% for this shared ownership type"]) + record.errors.clear + end + end + + it "doesn't add an error to stairbought and type if the percentage bought is less than the threshold (which is 1% by default, but higher for some shared ownership types)" do + record.stairbought = 10 + [2, 16, 18, 24].each do |type| + record.type = type + financial_validator.validate_percentage_bought_at_least_threshold(record) + expect(record.errors).to be_empty + record.errors.clear + end + + record.stairbought = 1 + [28, 30, 31, 32].each do |type| + record.type = type + financial_validator.validate_percentage_bought_at_least_threshold(record) + expect(record.errors).to be_empty + record.errors.clear + end + end + end + describe "#validate_percentage_owned_not_too_much_if_older_person" do let(:record) { FactoryBot.create(:sales_log) } diff --git a/spec/models/validations/sales/setup_validations_spec.rb b/spec/models/validations/sales/setup_validations_spec.rb index 4571ae5cd..e5c563c9e 100644 --- a/spec/models/validations/sales/setup_validations_spec.rb +++ b/spec/models/validations/sales/setup_validations_spec.rb @@ -55,7 +55,6 @@ RSpec.describe Validations::Sales::SetupValidations do example.run end Timecop.return - Singleton.__init__(FormHandler) end context "when saledate is blank" do diff --git a/spec/requests/lettings_logs_controller_spec.rb b/spec/requests/lettings_logs_controller_spec.rb index 9db2c3236..116c88c4c 100644 --- a/spec/requests/lettings_logs_controller_spec.rb +++ b/spec/requests/lettings_logs_controller_spec.rb @@ -108,7 +108,7 @@ RSpec.describe LettingsLogsController, type: :request do lettings_log_params.merge(org_params) { |_k, a_val, b_val| a_val.merge(b_val) } end - it "marks the record as completed" do + xit "marks the record as completed" do json_response = JSON.parse(response.body) expect(json_response).not_to have_key("errors") @@ -252,24 +252,36 @@ RSpec.describe LettingsLogsController, type: :request do end it "does have organisation values" do - get "/lettings-logs", headers: headers, params: {} + get "/lettings-logs", headers:, params: {} expect(page).to have_content("Owned by") expect(page).to have_content("Managed by") end it "shows lettings logs for all organisations" do - get "/lettings-logs", headers: headers, params: {} + get "/lettings-logs", headers:, params: {} expect(page).to have_content("LC783") expect(page).to have_content("UA984") end + it "displays CSV download links with the correct paths" do + get "/lettings-logs", headers:, params: {} + expect(page).to have_link("Download (CSV)", href: "/lettings-logs/csv-download?codes_only=false") + expect(page).to have_link("Download (CSV, codes only)", href: "/lettings-logs/csv-download?codes_only=true") + end + context "when there are no logs in the database" do before do LettingsLog.destroy_all end + it "does not display CSV download links" do + get "/lettings-logs", headers:, params: {} + expect(page).not_to have_link("Download (CSV)") + expect(page).not_to have_link("Download (CSV, codes only)") + end + it "page has correct title" do - get "/lettings-logs", headers: headers, params: {} + get "/lettings-logs", headers:, params: {} expect(page).to have_title("Logs - Submit social housing lettings and sales data (CORE) - GOV.UK") end end @@ -292,29 +304,29 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows lettings logs for multiple selected statuses" do - get "/lettings-logs?status[]=in_progress&status[]=completed", headers: headers, params: {} + get "/lettings-logs?status[]=in_progress&status[]=completed", headers:, params: {} expect(page).to have_link(in_progress_lettings_log.id.to_s) expect(page).to have_link(completed_lettings_log.id.to_s) end it "shows lettings logs for one selected status" do - get "/lettings-logs?status[]=in_progress", headers: headers, params: {} + get "/lettings-logs?status[]=in_progress", headers:, params: {} expect(page).to have_link(in_progress_lettings_log.id.to_s) expect(page).not_to have_link(completed_lettings_log.id.to_s) end it "filters on organisation" do - get "/lettings-logs?organisation[]=#{organisation_2.id}", headers: headers, params: {} + get "/lettings-logs?organisation[]=#{organisation_2.id}", headers:, params: {} expect(page).to have_link(completed_lettings_log.id.to_s) expect(page).not_to have_link(in_progress_lettings_log.id.to_s) end it "does not reset the filters" do - get "/lettings-logs?status[]=in_progress", headers: headers, params: {} + get "/lettings-logs?status[]=in_progress", headers:, params: {} expect(page).to have_link(in_progress_lettings_log.id.to_s) expect(page).not_to have_link(completed_lettings_log.id.to_s) - get "/lettings-logs", headers: headers, params: {} + get "/lettings-logs", headers:, params: {} expect(page).to have_link(in_progress_lettings_log.id.to_s) expect(page).not_to have_link(completed_lettings_log.id.to_s) end @@ -338,13 +350,13 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows lettings logs for multiple selected years" do - get "/lettings-logs?years[]=2021&years[]=2022", headers: headers, params: {} + get "/lettings-logs?years[]=2021&years[]=2022", headers:, params: {} expect(page).to have_link(lettings_log_2021.id.to_s) expect(page).to have_link(lettings_log_2022.id.to_s) end it "shows lettings logs for one selected year" do - get "/lettings-logs?years[]=2021", headers: headers, params: {} + get "/lettings-logs?years[]=2021", headers:, params: {} expect(page).to have_link(lettings_log_2021.id.to_s) expect(page).not_to have_link(lettings_log_2022.id.to_s) end @@ -387,14 +399,14 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows lettings logs for multiple selected statuses and years" do - get "/lettings-logs?years[]=2021&years[]=2022&status[]=in_progress&status[]=completed", headers: headers, params: {} + get "/lettings-logs?years[]=2021&years[]=2022&status[]=in_progress&status[]=completed", headers:, params: {} expect(page).to have_link(lettings_log_2021.id.to_s) expect(page).to have_link(lettings_log_2022.id.to_s) expect(page).to have_link(lettings_log_2022_in_progress.id.to_s) end it "shows lettings logs for one selected status" do - get "/lettings-logs?years[]=2022&status[]=in_progress", headers: headers, params: {} + get "/lettings-logs?years[]=2022&status[]=in_progress", headers:, params: {} expect(page).to have_link(lettings_log_2022_in_progress.id.to_s) expect(page).not_to have_link(lettings_log_2021.id.to_s) expect(page).not_to have_link(lettings_log_2022.id.to_s) @@ -512,23 +524,36 @@ RSpec.describe LettingsLogsController, type: :request do end it "does not have organisation columns" do - get "/lettings-logs", headers: headers, params: {} + get "/lettings-logs", headers:, params: {} expect(page).not_to have_content("Owning organisation") expect(page).not_to have_content("Managing organisation") end + it "displays standard CSV download link only, with the correct path" do + get "/lettings-logs", headers:, params: {} + expect(page).to have_link("Download (CSV)", href: "/lettings-logs/csv-download?codes_only=false") + expect(page).not_to have_link("Download (CSV, codes only)") + end + + it "does not display CSV download links if there are no logs" do + LettingsLog.destroy_all + get "/lettings-logs", headers:, params: {} + expect(page).not_to have_link("Download (CSV)") + expect(page).not_to have_link("Download (CSV, codes only)") + end + context "when using a search query" do let(:logs) { FactoryBot.create_list(:lettings_log, 3, :completed, owning_organisation: user.organisation, created_by: user) } let(:log_to_search) { FactoryBot.create(:lettings_log, :completed, owning_organisation: user.organisation, created_by: user) } let(:log_total_count) { LettingsLog.where(owning_organisation: user.organisation).count } it "has search results in the title" do - get "/lettings-logs?search=#{log_to_search.id}", headers: headers, params: {} + get "/lettings-logs?search=#{log_to_search.id}", headers:, params: {} expect(page).to have_title("Logs (1 logs matching ‘#{log_to_search.id}’) - Submit social housing lettings and sales data (CORE) - GOV.UK") end it "shows lettings logs matching the id" do - get "/lettings-logs?search=#{log_to_search.id}", headers: headers, params: {} + get "/lettings-logs?search=#{log_to_search.id}", headers:, params: {} expect(page).to have_link(log_to_search.id.to_s) logs.each do |log| expect(page).not_to have_link(log.id.to_s) @@ -536,7 +561,7 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows lettings logs matching the tenant code" do - get "/lettings-logs?search=#{log_to_search.tenancycode}", headers: headers, params: {} + get "/lettings-logs?search=#{log_to_search.tenancycode}", headers:, params: {} expect(page).to have_link(log_to_search.id.to_s) logs.each do |log| expect(page).not_to have_link(log.id.to_s) @@ -544,7 +569,7 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows lettings logs matching the property reference" do - get "/lettings-logs?search=#{log_to_search.propcode}", headers: headers, params: {} + get "/lettings-logs?search=#{log_to_search.propcode}", headers:, params: {} expect(page).to have_link(log_to_search.id.to_s) logs.each do |log| expect(page).not_to have_link(log.id.to_s) @@ -552,25 +577,27 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows lettings logs matching the property postcode" do - get "/lettings-logs?search=#{log_to_search.postcode_full}", headers: headers, params: {} + get "/lettings-logs?search=#{log_to_search.postcode_full}", headers:, params: {} expect(page).to have_link(log_to_search.id.to_s) logs.each do |log| expect(page).not_to have_link(log.id.to_s) end end - it "includes the search on the CSV link" do + it "includes the search on the CSV links" do search_term = "foo" FactoryBot.create(:lettings_log, created_by: user, owning_organisation: user.organisation, tenancycode: "foo") - get "/lettings-logs?search=#{search_term}", headers: headers, params: {} - expect(page).to have_link("Download (CSV)", href: "/lettings-logs/csv-download?search=#{search_term}") + get "/lettings-logs?search=#{search_term}", headers:, params: {} + download_link = page.find_link("Download (CSV)") + download_link_params = CGI.parse(URI.parse(download_link[:href]).query) + expect(download_link_params).to include("search" => [search_term]) end context "when more than one results with matching postcode" do let!(:matching_postcode_log) { FactoryBot.create(:lettings_log, :completed, owning_organisation: user.organisation, postcode_full: log_to_search.postcode_full) } it "displays all matching logs" do - get "/lettings-logs?search=#{log_to_search.postcode_full}", headers: headers, params: {} + get "/lettings-logs?search=#{log_to_search.postcode_full}", headers:, params: {} expect(page).to have_link(log_to_search.id.to_s) expect(page).to have_link(matching_postcode_log.id.to_s) logs.each do |log| @@ -585,12 +612,12 @@ RSpec.describe LettingsLogsController, type: :request do let(:log_total_count) { LettingsLog.where(owning_organisation: user.organisation).count } it "has title with pagination details for page 1" do - get "/lettings-logs?search=#{logs[0].postcode_full}", headers: headers, params: {} + get "/lettings-logs?search=#{logs[0].postcode_full}", headers:, params: {} expect(page).to have_title("Logs (#{logs.count} logs matching ‘#{postcode}’) (page 1 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK") end it "has title with pagination details for page 2" do - get "/lettings-logs?search=#{logs[0].postcode_full}&page=2", headers: headers, params: {} + get "/lettings-logs?search=#{logs[0].postcode_full}&page=2", headers:, params: {} expect(page).to have_title("Logs (#{logs.count} logs matching ‘#{postcode}’) (page 2 of 2) - Submit social housing lettings and sales data (CORE) - GOV.UK") end end @@ -621,7 +648,7 @@ RSpec.describe LettingsLogsController, type: :request do let!(:log_matching_filter_and_search) { FactoryBot.create(:lettings_log, :in_progress, owning_organisation: user.organisation, postcode_full: matching_postcode, created_by: user) } it "shows only logs matching both search and filters" do - get "/lettings-logs?search=#{matching_postcode}&status[]=#{matching_status}", headers: headers, params: {} + get "/lettings-logs?search=#{matching_postcode}&status[]=#{matching_status}", headers:, params: {} expect(page).to have_link(log_matching_filter_and_search.id.to_s) expect(page).not_to have_link(log_to_search.id.to_s) logs.each do |log| @@ -631,7 +658,7 @@ RSpec.describe LettingsLogsController, type: :request do end end - context "when there are less than 20 logs" do + context "when there are fewer than 20 logs" do before do get "/lettings-logs", headers:, params: {} end @@ -675,7 +702,7 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows the CSV download link" do - expect(page).to have_link("Download (CSV)", href: "/lettings-logs/csv-download") + expect(page).to have_link("Download (CSV)", href: "/lettings-logs/csv-download?codes_only=false") end it "does not show the organisation filter" do @@ -809,7 +836,7 @@ RSpec.describe LettingsLogsController, type: :request do context "with a user that is not signed in" do it "does not let the user get lettings log tasklist pages they don't have access to" do - get "/lettings-logs/#{lettings_log.id}", headers: headers, params: {} + get "/lettings-logs/#{lettings_log.id}", headers:, params: {} expect(response).to redirect_to("/account/sign-in") end end @@ -975,7 +1002,7 @@ RSpec.describe LettingsLogsController, type: :request do before do sign_in user - get "/lettings-logs/csv-download?search=#{search_term}", headers: + get "/lettings-logs/csv-download?search=#{search_term}&codes_only=false", headers: end it "returns http success" do @@ -1246,8 +1273,91 @@ RSpec.describe LettingsLogsController, type: :request do end end + describe "GET #csv-download" do + let(:page) { Capybara::Node::Simple.new(response.body) } + let(:user) { FactoryBot.create(:user) } + let(:headers) { { "Accept" => "text/html" } } + + before do + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in user + end + + it "renders a page with the correct header" do + get "/lettings-logs/csv-download?codes_only=false", headers:, params: {} + header = page.find_css("h1") + expect(header.text).to include("Download CSV") + end + + it "renders a form with the correct target containing a button with the correct text" do + get "/lettings-logs/csv-download?codes_only=false", headers:, params: {} + form = page.find("form.button_to") + expect(form[:method]).to eq("post") + expect(form[:action]).to eq("/lettings-logs/email-csv") + expect(form).to have_button("Send email") + end + + it "when query string contains search parameter, form contains hidden field with correct value" do + search_term = "blam" + get "/lettings-logs/csv-download?codes_only=false&search=#{search_term}", headers:, params: {} + hidden_field = page.find("form.button_to").find_field("search", type: "hidden") + expect(hidden_field.value).to eq(search_term) + end + + context "when the user is a data coordinator" do + let(:user) { FactoryBot.create(:user, :data_coordinator) } + + it "when codes_only query parameter is false, form contains hidden field with correct value" do + codes_only = false + get "/lettings-logs/csv-download?codes_only=#{codes_only}", headers:, params: {} + hidden_field = page.find("form.button_to").find_field("codes_only", type: "hidden") + expect(hidden_field.value).to eq(codes_only.to_s) + end + + it "when codes_only query parameter is true, user is not authorized" do + codes_only = true + get "/lettings-logs/csv-download?codes_only=#{codes_only}", headers:, params: {} + expect(response).to have_http_status(:unauthorized) + end + end + + context "when the user is a data provider" do + it "when codes_only query parameter is false, form contains hidden field with correct value" do + codes_only = false + get "/lettings-logs/csv-download?codes_only=#{codes_only}", headers:, params: {} + hidden_field = page.find("form.button_to").find_field("codes_only", type: "hidden") + expect(hidden_field.value).to eq(codes_only.to_s) + end + + it "when codes_only query parameter is true, user is not authorized" do + codes_only = true + get "/lettings-logs/csv-download?codes_only=#{codes_only}", headers:, params: {} + expect(response).to have_http_status(:unauthorized) + end + end + + context "when the user is a support user" do + let(:user) { FactoryBot.create(:user, :support) } + + it "when codes_only query parameter is false, form contains hidden field with correct value" do + codes_only = false + get "/lettings-logs/csv-download?codes_only=#{codes_only}", headers:, params: {} + hidden_field = page.find("form.button_to").find_field("codes_only", type: "hidden") + expect(hidden_field.value).to eq(codes_only.to_s) + end + + it "when codes_only query parameter is true, form contains hidden field with correct value" do + codes_only = true + get "/lettings-logs/csv-download?codes_only=#{codes_only}", headers:, params: {} + hidden_field = page.find("form.button_to").find_field("codes_only", type: "hidden") + expect(hidden_field.value).to eq(codes_only.to_s) + end + end + end + describe "POST #email-csv" do let(:other_organisation) { FactoryBot.create(:organisation) } + let(:user) { FactoryBot.create(:user, :support) } context "when a log exists" do let!(:lettings_log) do @@ -1259,6 +1369,7 @@ RSpec.describe LettingsLogsController, type: :request do end before do + allow(user).to receive(:need_two_factor_authentication?).and_return(false) sign_in user FactoryBot.create(:lettings_log) FactoryBot.create(:lettings_log, @@ -1270,33 +1381,59 @@ RSpec.describe LettingsLogsController, type: :request do it "creates an E-mail job" do expect { - post "/lettings-logs/email-csv", headers:, params: {} - }.to enqueue_job(EmailCsvJob).with(user, nil, {}, false) + post "/lettings-logs/email-csv?codes_only=true", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, nil, {}, false, nil, true) end it "redirects to the confirmation page" do - post "/lettings-logs/email-csv", headers:, params: {} + post "/lettings-logs/email-csv?codes_only=true", headers:, params: {} expect(response).to redirect_to(csv_confirmation_lettings_logs_path) end it "passes the search term" do expect { - post "/lettings-logs/email-csv?search=#{lettings_log.id}", headers:, params: {} - }.to enqueue_job(EmailCsvJob).with(user, lettings_log.id.to_s, {}, false) + post "/lettings-logs/email-csv?search=#{lettings_log.id}&codes_only=false", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, lettings_log.id.to_s, {}, false, nil, false) end it "passes filter parameters" do expect { - post "/lettings-logs/email-csv?status[]=completed", headers:, params: {} - }.to enqueue_job(EmailCsvJob).with(user, nil, { "status" => %w[completed] }, false) + post "/lettings-logs/email-csv?status[]=completed&codes_only=true", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, nil, { "status" => %w[completed] }, false, nil, true) + end + + it "passes export type flag" do + expect { + post "/lettings-logs/email-csv?codes_only=true", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, nil, {}, false, nil, true) + expect { + post "/lettings-logs/email-csv?codes_only=false", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, nil, {}, false, nil, false) end - it "passes a combination of search term and filter parameters" do + it "passes a combination of search term, export type and filter parameters" do postcode = "XX1 1TG" expect { - post "/lettings-logs/email-csv?status[]=completed&search=#{postcode}", headers:, params: {} - }.to enqueue_job(EmailCsvJob).with(user, postcode, { "status" => %w[completed] }, false) + post "/lettings-logs/email-csv?status[]=completed&search=#{postcode}&codes_only=false", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, postcode, { "status" => %w[completed] }, false, nil, false) + end + + context "when the user is not a support user" do + let(:user) { FactoryBot.create(:user, :data_coordinator) } + + it "has permission to download human readable csv" do + codes_only_export = false + expect { + post "/lettings-logs/email-csv?codes_only=#{codes_only_export}", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, nil, {}, false, nil, false) + end + + it "is not authorized to download codes only csv" do + codes_only_export = true + post "/lettings-logs/email-csv?codes_only=#{codes_only_export}", headers:, params: {} + expect(response).to have_http_status(:unauthorized) + end end end end diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb index 004ea538a..e7cb7fea3 100644 --- a/spec/requests/organisations_controller_spec.rb +++ b/spec/requests/organisations_controller_spec.rb @@ -1142,11 +1142,12 @@ RSpec.describe OrganisationsController, type: :request do context "when they view the logs tab" do before do FactoryBot.create(:lettings_log, owning_organisation: organisation) - get "/organisations/#{organisation.id}/lettings-logs" end - it "has a CSV download button with the correct path if at least 1 log exists" do - expect(page).to have_link("Download (CSV)", href: "/organisations/#{organisation.id}/logs/csv-download") + it "has CSV download buttons with the correct paths if at least 1 log exists" do + get "/organisations/#{organisation.id}/lettings-logs" + expect(page).to have_link("Download (CSV)", href: "/organisations/#{organisation.id}/logs/csv-download?codes_only=false") + expect(page).to have_link("Download (CSV, codes only)", href: "/organisations/#{organisation.id}/logs/csv-download?codes_only=true") end context "when you download the CSV" do @@ -1158,16 +1159,64 @@ RSpec.describe OrganisationsController, type: :request do end it "only includes logs from that organisation" do - get "/organisations/#{organisation.id}/logs/csv-download" + get "/organisations/#{organisation.id}/logs/csv-download?codes_only=false" expect(page).to have_text("You've selected 3 logs.") end it "provides the organisation to the mail job" do expect { - post "/organisations/#{organisation.id}/logs/email-csv?status[]=completed", headers:, params: {} - }.to enqueue_job(EmailCsvJob).with(user, nil, { "status" => %w[completed] }, false, organisation) + post "/organisations/#{organisation.id}/logs/email-csv?status[]=completed&codes_only=false", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, nil, { "status" => %w[completed] }, false, organisation, false) end + + it "provides the export type to the mail job" do + codes_only_export_type = false + expect { + post "/organisations/#{organisation.id}/logs/email-csv?codes_only=#{codes_only_export_type}", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, nil, {}, false, organisation, codes_only_export_type) + codes_only_export_type = true + expect { + post "/organisations/#{organisation.id}/logs/email-csv?codes_only=#{codes_only_export_type}", headers:, params: {} + }.to enqueue_job(EmailCsvJob).with(user, nil, {}, false, organisation, codes_only_export_type) + end + end + end + + describe "GET #download_csv" do + it "renders a page with the correct header" do + get "/organisations/#{organisation.id}/logs/csv-download?codes_only=false", headers:, params: {} + header = page.find_css("h1") + expect(header.text).to include("Download CSV") + end + + it "renders a form with the correct target containing a button with the correct text" do + get "/organisations/#{organisation.id}/logs/csv-download?codes_only=false", headers:, params: {} + form = page.find("form.button_to") + expect(form[:method]).to eq("post") + expect(form[:action]).to eq("/organisations/#{organisation.id}/logs/email-csv") + expect(form).to have_button("Send email") + end + + it "when codes_only query parameter is false, form contains hidden field with correct value" do + codes_only = false + get "/organisations/#{organisation.id}/logs/csv-download?codes_only=#{codes_only}", headers:, params: {} + hidden_field = page.find("form.button_to").find_field("codes_only", type: "hidden") + expect(hidden_field.value).to eq(codes_only.to_s) + end + + it "when codes_only query parameter is true, form contains hidden field with correct value" do + codes_only = true + get "/organisations/#{organisation.id}/logs/csv-download?codes_only=#{codes_only}", headers:, params: {} + hidden_field = page.find("form.button_to").find_field("codes_only", type: "hidden") + expect(hidden_field.value).to eq(codes_only.to_s) + end + + it "when query string contains search parameter, form contains hidden field with correct value" do + search_term = "blam" + get "/organisations/#{organisation.id}/logs/csv-download?codes_only=true&search=#{search_term}", headers:, params: {} + hidden_field = page.find("form.button_to").find_field("search", type: "hidden") + expect(hidden_field.value).to eq(search_term) end end diff --git a/spec/services/bulk_upload/lettings/log_creator_spec.rb b/spec/services/bulk_upload/lettings/log_creator_spec.rb index 39dfd00fe..8b320c35f 100644 --- a/spec/services/bulk_upload/lettings/log_creator_spec.rb +++ b/spec/services/bulk_upload/lettings/log_creator_spec.rb @@ -30,9 +30,9 @@ RSpec.describe BulkUpload::Lettings::LogCreator do let(:log) { LettingsLog.new } before do - file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) - file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) - file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2022_csv_row) file.rewind end @@ -61,7 +61,7 @@ RSpec.describe BulkUpload::Lettings::LogCreator do end before do - file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2022_csv_row) file.rewind end diff --git a/spec/services/bulk_upload/lettings/validator_spec.rb b/spec/services/bulk_upload/lettings/validator_spec.rb index ba48bd9e7..f7048fa27 100644 --- a/spec/services/bulk_upload/lettings/validator_spec.rb +++ b/spec/services/bulk_upload/lettings/validator_spec.rb @@ -28,6 +28,18 @@ RSpec.describe BulkUpload::Lettings::Validator do end end + context "when file has too few columns" do + before do + file.write("a," * 132) + file.write("\n") + file.rewind + end + + it "is not valid" do + expect(validator).not_to be_valid + end + end + context "when incorrect headers" end @@ -59,6 +71,46 @@ RSpec.describe BulkUpload::Lettings::Validator do end end + context "with arbitrary ordered 23/24 csv" do + let(:bulk_upload) { create(:bulk_upload, user:, year: 2023) } + let(:log) { build(:lettings_log, :completed) } + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:seed) { 321 } + + around do |example| + FormHandler.instance.use_real_forms! + + example.run + + FormHandler.instance.use_fake_forms! + end + + before do + file.write(BulkUpload::LogToCsv.new(log:, line_ending: "\r\n").default_2023_field_numbers_row(seed:)) + file.write(BulkUpload::LogToCsv.new(log:, line_ending: "\r\n").to_2023_csv_row(seed:)) + file.close + end + + it "creates validation errors" do + expect { validator.call }.to change(BulkUploadError, :count) + end + + it "create validation error with correct values" do + validator.call + + error = BulkUploadError.find_by(field: "field_5") + + expect(error.field).to eql("field_5") + expect(error.error).to eql("You must answer letting type") + expect(error.tenant_code).to eql(log.tenancycode) + expect(error.property_ref).to eql(log.propcode) + expect(error.row).to eql("2") + expect(error.cell).to eql("DD2") + expect(error.col).to eql("DD") + end + end + context "with unix line endings" do let(:fixture_path) { file_fixture("2022_23_lettings_bulk_upload.csv") } let(:file) { Tempfile.new } @@ -82,7 +134,7 @@ RSpec.describe BulkUpload::Lettings::Validator do let(:path) { file.path } before do - file.write(BulkUpload::LogToCsv.new(log:, line_ending: "\r\n", col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) file.close end @@ -122,8 +174,8 @@ RSpec.describe BulkUpload::Lettings::Validator do let(:log_2) { build(:lettings_log, :completed, created_by: user) } 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, overrides: { illness: 100 }).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0, overrides: { illness: 100 }).to_2022_csv_row) file.close end @@ -138,8 +190,8 @@ RSpec.describe BulkUpload::Lettings::Validator do let(:log_2) { build(:lettings_log, :completed, renttype: 1, created_by: user) } 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_1, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) file.close end @@ -155,7 +207,7 @@ RSpec.describe BulkUpload::Lettings::Validator do let(:log_1) { build(:lettings_log, :completed, renttype: 1, created_by: user, owning_organisation: unaffiliated_org) } before do - file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) file.close end @@ -169,7 +221,7 @@ RSpec.describe BulkUpload::Lettings::Validator do let(:log) { build(:lettings_log, :in_progress, created_by: user, startdate: Time.zone.local(2022, 5, 1)) } before do - file.write(BulkUpload::LogToCsv.new(log:, line_ending: "\r\n", col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) file.close end @@ -188,11 +240,11 @@ 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) + file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) file.close end @@ -210,11 +262,11 @@ 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) + file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) file.close end @@ -238,11 +290,11 @@ 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) + file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0).to_2022_csv_row) file.close end @@ -262,11 +314,11 @@ RSpec.describe BulkUpload::Lettings::Validator do before do 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.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0, overrides:).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0, overrides:).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0, overrides:).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0, overrides:).to_2022_csv_row) + file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0, overrides:).to_2022_csv_row) file.close end diff --git a/spec/services/bulk_upload/lettings/csv_parser_spec.rb b/spec/services/bulk_upload/lettings/year2022/csv_parser_spec.rb similarity index 67% rename from spec/services/bulk_upload/lettings/csv_parser_spec.rb rename to spec/services/bulk_upload/lettings/year2022/csv_parser_spec.rb index 80c8e37dc..d396ad78e 100644 --- a/spec/services/bulk_upload/lettings/csv_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2022/csv_parser_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe BulkUpload::Lettings::CsvParser do +RSpec.describe BulkUpload::Lettings::Year2022::CsvParser do subject(:service) { described_class.new(path:) } let(:path) { file_fixture("2022_23_lettings_bulk_upload.csv") } @@ -22,7 +22,7 @@ RSpec.describe BulkUpload::Lettings::CsvParser do let(:log) { build(:lettings_log, :completed) } before do - file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2022_csv_row) file.rewind end @@ -44,7 +44,7 @@ RSpec.describe BulkUpload::Lettings::CsvParser do before do file.write(bom) - file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2022_csv_row) file.close end @@ -61,7 +61,7 @@ RSpec.describe BulkUpload::Lettings::CsvParser do before do file.write(invalid_sequence) - file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_csv_row) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2022_csv_row) file.close end @@ -69,4 +69,29 @@ RSpec.describe BulkUpload::Lettings::CsvParser do expect(service.row_parsers[0].field_12.to_i).to eql(log.age1) end end + + describe "#column_for_field", aggregate_failures: true do + context "when headers present" do + it "returns correct column" do + expect(service.column_for_field("field_1")).to eql("B") + expect(service.column_for_field("field_134")).to eql("EE") + end + end + + context "when no headers" do + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { build(:lettings_log, :completed) } + + before do + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2022_csv_row) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_1")).to eql("A") + expect(service.column_for_field("field_134")).to eql("ED") + end + end + end end diff --git a/spec/services/bulk_upload/lettings/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2022/row_parser_spec.rb similarity index 99% rename from spec/services/bulk_upload/lettings/row_parser_spec.rb rename to spec/services/bulk_upload/lettings/year2022/row_parser_spec.rb index 68990c59b..312a63a3b 100644 --- a/spec/services/bulk_upload/lettings/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2022/row_parser_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe BulkUpload::Lettings::RowParser do +RSpec.describe BulkUpload::Lettings::Year2022::RowParser do subject(:parser) { described_class.new(attributes) } let(:now) { Time.zone.today } diff --git a/spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb b/spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb new file mode 100644 index 000000000..a8cf227f4 --- /dev/null +++ b/spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb @@ -0,0 +1,181 @@ +require "rails_helper" + +RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do + subject(:service) { described_class.new(path:) } + + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { build(:lettings_log, :completed) } + + context "when parsing csv with headers" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LogToCsv.new(log:).default_2023_field_numbers_row) + file.write(BulkUpload::LogToCsv.new(log:).to_2023_csv_row) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with headers with extra rows" do + before do + file.write("Section\n") + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LogToCsv.new(log:).default_2023_field_numbers_row) + file.write(BulkUpload::LogToCsv.new(log:).to_2023_csv_row) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(8) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with headers in arbitrary order" do + let(:seed) { rand } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LogToCsv.new(log:).default_2023_field_numbers_row(seed:)) + file.write(BulkUpload::LogToCsv.new(log:).to_2023_csv_row(seed:)) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv without headers" do + before do + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2023_csv_row) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(0) + expect(service.col_offset).to eq(0) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing with BOM aka byte order mark" do + let(:bom) { "\uFEFF" } + + before do + file.write(bom) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2023_csv_row) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when an invalid byte sequence" do + let(:invalid_sequence) { "\x81" } + + before do + file.write(invalid_sequence) + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2023_csv_row) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + describe "#column_for_field", aggregate_failures: true do + context "when with headers using default ordering" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LogToCsv.new(log:).default_2023_field_numbers_row) + file.write(BulkUpload::LogToCsv.new(log:).to_2023_csv_row) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("B") + expect(service.column_for_field("field_22")).to eql("EL") + end + end + + context "when without headers using default ordering" do + before do + file.write(BulkUpload::LogToCsv.new(log:, col_offset: 0).to_2023_csv_row) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("A") + expect(service.column_for_field("field_22")).to eql("EK") + end + end + + context "when with headers using custom ordering" do + let(:seed) { 123 } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LogToCsv.new(log:).default_2023_field_numbers_row(seed:)) + file.write(BulkUpload::LogToCsv.new(log:).to_2023_csv_row(seed:)) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("N") + expect(service.column_for_field("field_22")).to eql("O") + expect(service.column_for_field("field_26")).to eql("B") + expect(service.column_for_field("field_25")).to eql("EF") + end + end + end +end diff --git a/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb new file mode 100644 index 000000000..f658f7b04 --- /dev/null +++ b/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb @@ -0,0 +1,1282 @@ +require "rails_helper" + +RSpec.describe BulkUpload::Lettings::Year2023::RowParser do + subject(:parser) { described_class.new(attributes) } + + let(:now) { Time.zone.today } + + let(:attributes) { { bulk_upload: } } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: nil) } + let(:user) { create(:user, organisation: owning_org) } + + let(:owning_org) { create(:organisation, :with_old_visible_id) } + let(:managing_org) { create(:organisation, :with_old_visible_id) } + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:location) { create(:location, :with_old_visible_id, scheme:) } + + let(:setup_section_params) do + { + bulk_upload:, + field_1: owning_org.old_visible_id, + field_2: managing_org.old_visible_id, + field_4: "1", + field_5: "2", + field_6: "2", + field_7: now.day.to_s, + field_8: now.month.to_s, + field_9: now.strftime("%g"), + } + end + + before do + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org) + end + + around do |example| + FormHandler.instance.use_real_forms! + + example.run + + FormHandler.instance.use_fake_forms! + end + + describe "#blank_row?" do + context "when a new object" do + it "returns true" do + expect(parser).to be_blank_row + end + end + + context "when any field is populated" do + before do + parser.field_1 = "1" + end + + it "returns false" do + expect(parser).not_to be_blank_row + end + end + end + + describe "validations" do + before do + stub_request(:get, /api.postcodes.io/) + .to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"Manchester\", \"codes\":{\"admin_district\": \"E08000003\"}}}", headers: {}) + + parser.valid? + end + + describe "#valid?" do + context "when the row is blank" do + let(:attributes) { { bulk_upload: } } + + it "returns true" do + expect(parser).to be_valid + end + end + + context "when calling the method multiple times" do + let(:attributes) { { bulk_upload:, field_134: 2 } } + + it "does not add keep adding errors to the pile" do + expect { parser.valid? }.not_to change(parser.errors, :count) + end + end + + context "when valid row" do + let(:attributes) do + { + bulk_upload:, + field_5: "1", + field_13: "123", + field_7: now.day.to_s, + field_8: now.month.to_s, + field_9: now.strftime("%g"), + field_23: "EC1N", + field_24: "2TD", + field_1: owning_org.old_visible_id, + field_2: managing_org.old_visible_id, + field_11: "1", + field_6: "2", + field_29: "2", + field_30: "1", + field_31: "1", + field_32: "1", + field_39: "2", + field_40: "1", + field_41: "2", + field_45: "1", + + field_46: "42", + field_52: "41", + field_56: "20", + field_60: "18", + field_64: "16", + field_68: "14", + field_72: "12", + field_76: "20", + + field_47: "F", + field_53: "M", + field_57: "F", + field_61: "M", + field_65: "F", + field_69: "M", + field_73: "F", + field_77: "M", + + field_48: "17", + field_49: "18", + + field_51: "P", + field_55: "C", + field_59: "X", + field_63: "R", + field_67: "C", + field_71: "C", + field_75: "X", + + field_50: "1", + field_54: "2", + field_58: "6", + field_62: "7", + field_66: "8", + field_70: "9", + field_74: "0", + field_78: "10", + + field_79: "1", + field_80: "4", + field_81: "1", + + field_82: "1", + + field_89: "2", + + field_100: "5", + field_101: "2", + field_102: "31", + field_104: "3", + field_105: "12", + + field_106: "1", + field_107: "EC1N", + field_108: "2TD", + + field_110: "1", + field_111: "1", + field_112: "", + field_113: "1", + field_114: "", + field_115: "", + + field_116: "1", + field_117: "2", + field_118: "2", + + field_119: "2", + + field_120: "1", + field_122: "2000", + field_121: "2", + field_123: "1", + field_124: "1", + + field_126: "4", + field_128: "1234.56", + field_129: "43.32", + field_130: "13.14", + field_131: "101.11", + field_132: "1500.19", + field_133: "1", + field_134: "234.56", + + field_27: "15", + field_28: "0", + field_33: now.day.to_s, + field_34: now.month.to_s, + field_35: now.strftime("%g"), + + field_4: "1", + } + end + + it "returns true" do + expect(parser).to be_valid + end + + it "instantiates a log with everything completed", aggregate_failures: true do + questions = parser.send(:questions).reject do |q| + parser.send(:log).optional_fields.include?(q.id) || q.completed?(parser.send(:log)) + end + + expect(questions.map(&:id).size).to eq(0) + expect(questions.map(&:id)).to eql([]) + end + end + end + + context "when setup section not complete" do + let(:attributes) { { bulk_upload:, field_13: "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_4 field_5 field_7 field_8 field_9 field_1 field_2]) + end + end + + describe "#field_5" do + context "when null" do + let(:attributes) { { bulk_upload:, field_5: nil, field_15: "1" } } + + it "returns an error" do + expect(parser.errors[:field_5]).to be_present + end + end + + context "when incorrect data type" do + let(:attributes) { { bulk_upload:, field_5: "foo" } } + + it "returns an error" do + expect(parser.errors[:field_5]).to be_present + end + end + + context "when unpermitted value" do + let(:attributes) { { bulk_upload:, field_5: "101" } } + + it "returns an error" do + expect(parser.errors[:field_5]).to be_present + end + end + + context "when valid" do + let(:attributes) { { bulk_upload:, field_5: "1", field_4: "1" } } + + it "does not return any errors" do + expect(parser.errors[:field_5]).to be_blank + end + end + + context "when bulk upload is for general needs" do + context "when general needs option selected" do + let(:attributes) { { bulk_upload:, field_5: "1", field_4: "1" } } + + it "is permitted" do + expect(parser.errors[:field_5]).to be_blank + end + end + + context "when supported housing option selected" do + let(:attributes) { { bulk_upload:, field_5: "2", field_4: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_5]).to include("Lettings type must be a general needs type because you selected general needs when uploading the file") + end + end + end + + context "when bulk upload is for supported housing" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:) } + + context "when general needs option selected" do + let(:attributes) { { bulk_upload:, field_5: "1", field_4: "2" } } + + it "is not permitted" do + expect(parser.errors[:field_5]).to include("Lettings type must be a supported housing type because you selected supported housing when uploading the file") + end + end + + context "when supported housing option selected" do + let(:attributes) { { bulk_upload:, field_5: "2", field_4: "2" } } + + it "is permitted" do + expect(parser.errors[:field_5]).to be_blank + end + end + end + end + + describe "#field_16" do + context "when nullable not permitted" do + let(:attributes) { { bulk_upload:, field_5: "2", field_16: nil } } + + it "cannot be nulled" do + expect(parser.errors[:field_16]).to be_present + end + end + + context "when nullable permitted" do + let(:attributes) { { bulk_upload:, field_5: "1", field_16: nil } } + + it "can be nulled" do + expect(parser.errors[:field_15]).to be_blank + end + end + + context "when matching scheme cannot be found" do + let(:attributes) { { bulk_upload:, field_5: "1", field_16: "123" } } + + it "returns an error" do + expect(parser.errors[:field_16]).to be_present + end + end + + context "when scheme belongs to someone else" do + let(:other_scheme) { create(:scheme, :with_old_visible_id) } + let(:attributes) { { bulk_upload:, field_5: "1", field_16: other_scheme.old_visible_id, field_1: owning_org.old_visible_id } } + + it "returns an error" do + expect(parser.errors[:field_16]).to include("This management group code does not belong to your organisation, or any of your stock owners / managing agents") + end + end + + context "when scheme belongs to owning org" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:attributes) { { bulk_upload:, field_5: "1", field_16: scheme.old_visible_id, field_1: owning_org.old_visible_id } } + + it "does not return an error" do + expect(parser.errors[:field_16]).to be_blank + end + end + + context "when scheme belongs to managing org" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: managing_org) } + let(:attributes) { { bulk_upload:, field_5: "1", field_16: scheme.old_visible_id, field_2: managing_org.old_visible_id } } + + it "does not return an error" do + expect(parser.errors[:field_16]).to be_blank + end + end + end + + describe "#field_17" do + context "when location does not exist" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:attributes) do + { + bulk_upload:, + field_5: "1", + field_16: scheme.old_visible_id, + field_17: "dontexist", + field_1: owning_org.old_visible_id, + } + end + + it "returns an error" do + expect(parser.errors[:field_17]).to be_present + end + end + + context "when location exists" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:attributes) do + { + bulk_upload:, + field_5: "1", + field_16: scheme.old_visible_id, + field_17: location.old_visible_id, + field_1: owning_org.old_visible_id, + } + end + + it "does not return an error" do + expect(parser.errors[:field_17]).to be_blank + end + end + + context "when location exists but not related" do + let(:location) { create(:scheme, :with_old_visible_id) } + let(:attributes) do + { + bulk_upload:, + field_5: "1", + field_16: scheme.old_visible_id, + field_17: location.old_visible_id, + field_1: owning_org.old_visible_id, + } + end + + it "returns an error" do + expect(parser.errors[:field_17]).to be_present + end + end + end + + describe "#field_102" do # leaving reason + context "when field_6 is 1 meaning it is a renewal" do + context "when field_102 is 40" do + let(:attributes) { { bulk_upload:, field_102: "40", field_6: "1" } } + + it "is permitted" do + expect(parser.errors[:field_102]).to be_blank + end + end + + context "when field_102 is 42" do + let(:attributes) { { bulk_upload:, field_102: "42", field_6: "1" } } + + it "is permitted" do + expect(parser.errors[:field_102]).to be_blank + end + end + + context "when field_102 is not 40 or 42" do + let(:attributes) { { bulk_upload:, field_102: "1", field_6: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_102]).to be_present + end + end + end + end + + describe "#field_83, #field_84, #field_85" do + context "when more than one item selected" do + let(:attributes) { { bulk_upload:, field_83: "1", field_84: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_83]).to be_present + expect(parser.errors[:field_84]).to be_present + expect(parser.errors[:field_85]).to be_present + end + end + end + + describe "#field_87" do + context "when 1 and another disability field selected" do + let(:attributes) { { bulk_upload:, field_87: "1", field_86: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_87]).to be_present + end + end + end + + describe "#field_88" do + context "when 1 and another disability field selected" do + let(:attributes) { { bulk_upload:, field_88: "1", field_86: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_88]).to be_present + end + end + end + + describe "#field_87, #field_88" do + context "when both 1" do + let(:attributes) { { bulk_upload:, field_87: "1", field_88: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_87]).to be_present + expect(parser.errors[:field_88]).to be_present + end + end + end + + describe "#field_119" do # referral + context "when 3 ie PRP nominated by LA and owning org is LA" do + let(:attributes) { { bulk_upload:, field_119: "3", field_1: owning_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_119]).to be_present + end + end + + context "when 4 ie referred by LA and is general needs and owning org is LA" do + let(:attributes) { { bulk_upload:, field_119: "4", field_1: owning_org.old_visible_id.to_s, field_4: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_119]).to be_present + end + end + + context "when 4 ie referred by LA and is general needs and owning org is PRP" do + let(:owning_org) { create(:organisation, :prp, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_119: "4", field_1: owning_org.old_visible_id.to_s } } + + it "is permitted" do + expect(parser.errors[:field_119]).to be_blank + end + end + + context "when 4 ie referred by LA and is not general needs" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:) } + let(:attributes) { { bulk_upload:, field_119: "4", field_4: "2" } } + + it "is permitted" do + expect(parser.errors[:field_119]).to be_blank + end + end + end + + describe "fields 7, 8, 9 => startdate" do + context "when any one of these fields is blank" do + let(:attributes) { { bulk_upload:, field_5: "1", field_7: nil, field_8: nil, field_9: nil } } + + it "returns an error" do + parser.valid? + + expect(parser.errors[:field_7]).to be_present + expect(parser.errors[:field_8]).to be_present + expect(parser.errors[:field_9]).to be_present + end + end + + context "when field_9 is 4 digits instead of 2" do + let(:attributes) { { bulk_upload:, field_9: "2022" } } + + it "returns an error" do + parser.valid? + + expect(parser.errors[:field_9]).to include("Tenancy start year must be 2 digits") + end + end + + context "when invalid date given" do + let(:attributes) { { bulk_upload:, field_5: "1", field_7: "a", field_8: "12", field_9: "22" } } + + it "does not raise an error" do + expect { parser.valid? }.not_to raise_error + end + end + + context "when inside of collection year" do + let(:attributes) { { bulk_upload:, field_7: "1", field_8: "10", field_9: "22" } } + + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year: 2022) } + + it "does not return errors" do + parser.valid? + + expect(parser.errors[:field_7]).not_to be_present + expect(parser.errors[:field_8]).not_to be_present + expect(parser.errors[:field_9]).not_to be_present + end + end + + context "when outside of collection year" do + around do |example| + Timecop.freeze(Date.new(2022, 4, 2)) do + example.run + end + end + + let(:attributes) { { bulk_upload:, field_7: "1", field_8: "1", field_9: "22" } } + + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year: 2022) } + + it "returns errors" do + parser.valid? + + expect(parser.errors[:field_7]).to be_present + expect(parser.errors[:field_8]).to be_present + expect(parser.errors[:field_9]).to be_present + end + end + end + + describe "#field_1" do # owning org + context "when cannot find owning org" do + let(:attributes) { { bulk_upload:, field_1: "donotexist" } } + + it "is not permitted" do + expect(parser.errors[:field_1]).to eql(["The owning organisation code is incorrect"]) + end + end + + context "when org is not stock owning" do + let(:owning_org) { create(:organisation, :with_old_visible_id, :does_not_own_stock) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_1]).to eql(["The owning organisation code provided is for an organisation that does not own stock"]) + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when not affiliated with owning org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_1: unaffiliated_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_1]).to eql(["You do not have permission to add logs for this owning organisation"]) + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + end + + describe "#field_2" do # managing org + context "when cannot find managing org" do + let(:attributes) { { bulk_upload:, field_2: "donotexist" } } + + it "is not permitted" do + expect(parser.errors[:field_2]).to eql(["The managing organisation code is incorrect"]) + end + end + + context "when not affiliated with managing org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: unaffiliated_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_2]).to eql(["This managing organisation does not have a relationship with the owning organisation"]) + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + end + + describe "#field_4" do + context "when blank" do + let(:attributes) { { bulk_upload:, field_4: nil, field_13: "123" } } + + it "is reported as a setup error" do + errors = parser.errors.select { |e| e.options[:category] == :setup } + error = errors.find { |e| e.attribute == :field_4 } + + expect(error).to be_present + expect(error.type).to eql("You must answer needs type") + end + end + end + + describe "#field_6" do + context "when an unpermitted value" do + let(:attributes) { { bulk_upload:, field_6: "3" } } + + it "has errors on the field" do + expect(parser.errors[:field_6]).to be_present + end + end + end + + describe "#field_30" do + context "when null" do + let(:attributes) { setup_section_params.merge({ field_30: nil }) } + + it "returns an error" do + expect(parser.errors[:field_30]).to be_present + end + + it "populates with correct error message" do + expect(parser.errors[:field_30]).to eql(["You must answer type of building"]) + end + end + + context "when unpermitted values" do + let(:attributes) { setup_section_params.merge({ field_30: "4" }) } + + it "returns an error" do + expect(parser.errors[:field_30]).to be_present + end + end + end + end + + describe "#log" do + [ + %w[age1_known age1 field_46], + %w[age2_known age2 field_52], + %w[age3_known age3 field_56], + %w[age4_known age4 field_60], + %w[age5_known age5 field_64], + %w[age6_known age6 field_68], + %w[age7_known age7 field_72], + %w[age8_known age8 field_76], + ].each do |known, age, field| + describe "##{known} and ##{age}" do + context "when #{field} is R" do + let(:attributes) { { bulk_upload:, field.to_s => "R" } } + + it "sets ##{known} 1" do + expect(parser.log.public_send(known)).to be(1) + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end + + context "when #{field} is a number" do + let(:attributes) { { bulk_upload:, field.to_s => "50" } } + + it "sets ##{known} to 0" do + expect(parser.log.public_send(known)).to be(0) + end + + it "sets ##{age} to given age" do + expect(parser.log.public_send(age)).to be(50) + end + end + end + end + + describe "#location" do + context "when lookup is via new core id" do + let(:attributes) { { bulk_upload:, field_16: scheme.old_visible_id, field_17: location.id, field_1: owning_org } } + + it "assigns the correct location" do + expect(parser.log.location).to eql(location) + end + end + end + + describe "#scheme" do + context "when lookup is via id prefixed with S" do + let(:attributes) { { bulk_upload:, field_16: "S#{scheme.id}", field_1: owning_org } } + + it "assigns the correct scheme" do + expect(parser.log.scheme).to eql(scheme) + end + end + end + + describe "#owning_organisation" do + context "when lookup is via id prefixed with ORG" do + let(:attributes) { { bulk_upload:, field_1: "ORG#{owning_org.id}" } } + + it "assigns the correct org" do + expect(parser.log.owning_organisation).to eql(owning_org) + end + end + end + + describe "#managing_organisation" do + context "when lookup is via id prefixed with ORG" do + let(:attributes) { { bulk_upload:, field_2: "ORG#{managing_org.id}" } } + + it "assigns the correct org" do + expect(parser.log.managing_organisation).to eql(managing_org) + end + end + end + + describe "#cbl" do + context "when field_116 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_116: 1 } } + + it "sets value to 1" do + expect(parser.log.cbl).to be(1) + end + end + + context "when field_116 is no ie 2" do + let(:attributes) { { bulk_upload:, field_116: 2 } } + + it "sets value to 0" do + expect(parser.log.cbl).to be(0) + end + end + end + + describe "#chr" do + context "when field_118 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_118: 1 } } + + it "sets value to 1" do + expect(parser.log.chr).to be(1) + end + end + + context "when field_118 is no ie 2" do + let(:attributes) { { bulk_upload:, field_118: 2 } } + + it "sets value to 0" do + expect(parser.log.chr).to be(0) + end + end + end + + describe "#cap" do + context "when field_117 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_117: 1 } } + + it "sets value to 1" do + expect(parser.log.cap).to be(1) + end + end + + context "when field_117 is no ie 2" do + let(:attributes) { { bulk_upload:, field_117: 2 } } + + it "sets value to 0" do + expect(parser.log.cap).to be(0) + end + end + end + + describe "#letting_allocation_unknown" do + context "when field_116, 117, 118 are no ie 2" do + let(:attributes) { { bulk_upload:, field_116: 2, field_117: 2, field_118: 2 } } + + it "sets value to 1" do + expect(parser.log.letting_allocation_unknown).to be(1) + end + end + + context "when any one of field_116, 117, 118 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_116: 1 } } + + it "sets value to 0" do + expect(parser.log.letting_allocation_unknown).to be(0) + end + end + end + + describe "#renewal" do + context "when field_6 is no ie 2" do + let(:attributes) { { bulk_upload:, field_6: 2 } } + + it "sets value to 0" do + expect(parser.log.renewal).to eq(0) + end + end + + context "when field_6 is null but rsnvac/field_27 is 14" do + let(:attributes) { { bulk_upload:, field_6: "", field_27: "14" } } + + it "sets renewal to 1" do + expect(parser.log.renewal).to eq(1) + end + end + end + + describe "#sexN fields" do + let(:attributes) do + { + bulk_upload:, + field_47: "F", + field_53: "M", + field_57: "X", + field_61: "R", + field_65: "F", + field_69: "M", + field_73: "X", + field_77: "R", + } + end + + it "sets value from correct mapping" do + expect(parser.log.sex1).to eql("F") + expect(parser.log.sex2).to eql("M") + expect(parser.log.sex3).to eql("X") + expect(parser.log.sex4).to eql("R") + expect(parser.log.sex5).to eql("F") + expect(parser.log.sex6).to eql("M") + expect(parser.log.sex7).to eql("X") + expect(parser.log.sex8).to eql("R") + end + end + + describe "#ecstatN fields" do + let(:attributes) do + { + bulk_upload:, + field_50: "1", + field_54: "2", + field_58: "6", + field_62: "7", + field_66: "8", + field_70: "9", + field_74: "0", + field_78: "10", + } + end + + it "sets value from correct mapping", aggregate_failures: true do + expect(parser.log.ecstat1).to eq(1) + expect(parser.log.ecstat2).to eq(2) + expect(parser.log.ecstat3).to eq(6) + expect(parser.log.ecstat4).to eq(7) + expect(parser.log.ecstat5).to eq(8) + expect(parser.log.ecstat6).to eq(9) + expect(parser.log.ecstat7).to eq(0) + expect(parser.log.ecstat8).to eq(10) + end + end + + describe "#relatN fields" do + let(:attributes) do + { + bulk_upload:, + field_51: "P", + field_55: "C", + field_59: "X", + field_63: "R", + field_67: "P", + field_71: "C", + field_75: "X", + } + end + + it "sets value from correct mapping", aggregate_failures: true do + expect(parser.log.relat2).to eq("P") + expect(parser.log.relat3).to eq("C") + expect(parser.log.relat4).to eq("X") + expect(parser.log.relat5).to eq("R") + expect(parser.log.relat6).to eq("P") + expect(parser.log.relat7).to eq("C") + expect(parser.log.relat8).to eq("X") + end + end + + describe "#net_income_known" do + let(:attributes) { { bulk_upload:, field_120: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.net_income_known).to eq(0) + end + end + + describe "#unitletas" do + let(:attributes) { { bulk_upload:, field_26: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.unitletas).to eq(1) + end + end + + describe "#rsnvac" do + let(:attributes) { { bulk_upload:, field_27: "5" } } + + it "sets value from correct mapping" do + expect(parser.log.rsnvac).to eq(5) + end + end + + describe "#sheltered" do + let(:attributes) { { bulk_upload:, field_44: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.sheltered).to eq(1) + end + end + + describe "illness fields" do + mapping = [ + { attribute: :illness_type_1, field: :field_98 }, + { attribute: :illness_type_2, field: :field_92 }, + { attribute: :illness_type_3, field: :field_95 }, + { attribute: :illness_type_4, field: :field_90 }, + { attribute: :illness_type_5, field: :field_91 }, + { attribute: :illness_type_6, field: :field_93 }, + { attribute: :illness_type_7, field: :field_94 }, + { attribute: :illness_type_8, field: :field_97 }, + { attribute: :illness_type_9, field: :field_96 }, + { attribute: :illness_type_10, field: :field_99 }, + ] + + mapping.each do |hash| + describe "##{hash[:attribute]}" do + context "when yes" do + let(:attributes) { { bulk_upload:, hash[:field] => "1" } } + + it "sets value from correct mapping" do + expect(parser.log.public_send(hash[:attribute])).to eq(1) + end + end + + context "when no" do + let(:attributes) { { bulk_upload:, hash[:field] => "" } } + + it "sets value from correct mapping" do + expect(parser.log.public_send(hash[:attribute])).to be_nil + end + end + end + end + end + + describe "#irproduct_other" do + let(:attributes) { { bulk_upload:, field_12: "some other product" } } + + it "sets value to given free text string" do + expect(parser.log.irproduct_other).to eql("some other product") + end + end + + describe "#tenancyother" do + let(:attributes) { { bulk_upload:, field_42: "some other tenancy" } } + + it "sets value to given free text string" do + expect(parser.log.tenancyother).to eql("some other tenancy") + end + end + + describe "#tenancylength" do + let(:attributes) { { bulk_upload:, field_43: "2" } } + + it "sets value to given free text string" do + expect(parser.log.tenancylength).to eq(2) + end + end + + describe "#earnings" do + let(:attributes) { { bulk_upload:, field_122: "104.50" } } + + it "rounds to the nearest whole pound" do + expect(parser.log.earnings).to eq(105) + end + end + + describe "#reasonother" do + let(:attributes) { { bulk_upload:, field_103: "some other reason" } } + + it "sets value to given free text string" do + expect(parser.log.reasonother).to eql("some other reason") + end + end + + describe "#ppcodenk" do + let(:attributes) { { bulk_upload:, field_106: "2" } } + + it "sets correct value from mapping" do + expect(parser.log.ppcodenk).to eq(0) + end + end + + describe "#household_charge" do + let(:attributes) { { bulk_upload:, field_125: "1" } } + + it "sets correct value from mapping" do + expect(parser.log.household_charge).to eq(1) + end + end + + describe "#chcharge" do + let(:attributes) { { bulk_upload:, field_127: "123.45" } } + + it "sets value given" do + expect(parser.log.chcharge).to eq(123.45) + end + end + + describe "#tcharge" do + let(:attributes) { { bulk_upload:, field_132: "123.45" } } + + it "sets value given" do + expect(parser.log.tcharge).to eq(123.45) + end + end + + describe "#supcharg" do + let(:attributes) { { bulk_upload:, field_131: "123.45" } } + + it "sets value given" do + expect(parser.log.supcharg).to eq(123.45) + end + end + + describe "#pscharge" do + let(:attributes) { { bulk_upload:, field_130: "123.45" } } + + it "sets value given" do + expect(parser.log.pscharge).to eq(123.45) + end + end + + describe "#scharge" do + let(:attributes) { { bulk_upload:, field_129: "123.45" } } + + it "sets value given" do + expect(parser.log.scharge).to eq(123.45) + end + end + + describe "#offered" do + let(:attributes) { { bulk_upload:, field_28: "3" } } + + it "sets value given" do + expect(parser.log.offered).to eq(3) + end + end + + describe "#propcode" do + let(:attributes) { { bulk_upload:, field_14: "abc123" } } + + it "sets value given" do + expect(parser.log.propcode).to eq("abc123") + end + end + + describe "#mrcdate" do + let(:attributes) { { bulk_upload:, field_36: "13", field_37: "12", field_38: "22" } } + + it "sets value given" do + expect(parser.log.mrcdate).to eq(Date.new(2022, 12, 13)) + end + end + + describe "#majorrepairs" do + context "when mrcdate given" do + let(:attributes) { { bulk_upload:, field_36: "13", field_37: "12", field_38: "22" } } + + it "sets #majorrepairs to 1" do + expect(parser.log.majorrepairs).to eq(1) + end + end + + context "when mrcdate not given" do + let(:attributes) { { bulk_upload:, field_36: "", field_37: "", field_38: "" } } + + it "sets #majorrepairs to 0" do + expect(parser.log.majorrepairs).to eq(0) + end + end + end + + describe "#voiddate" do + let(:attributes) { { bulk_upload:, field_33: "13", field_34: "12", field_35: "22" } } + + it "sets value given" do + expect(parser.log.voiddate).to eq(Date.new(2022, 12, 13)) + end + end + + describe "#startdate" do + let(:attributes) { { bulk_upload:, field_7: now.day.to_s, field_8: now.month.to_s, field_9: now.strftime("%g") } } + + it "sets value given" do + expect(parser.log.startdate).to eq(now) + end + end + + describe "#postcode_full" do + let(:attributes) { { bulk_upload:, field_23: " EC1N ", field_24: " 2TD " } } + + it "strips whitespace" do + expect(parser.log.postcode_full).to eql("EC1N 2TD") + end + end + + describe "#la" do + let(:attributes) { { bulk_upload:, field_25: "E07000223" } } + + it "sets to given value" do + expect(parser.log.la).to eql("E07000223") + end + end + + describe "#prevloc" do + let(:attributes) { { bulk_upload:, field_109: "E07000223" } } + + it "sets to given value" do + expect(parser.log.prevloc).to eql("E07000223") + end + end + + describe "#previous_la_known" do + context "when known" do + let(:attributes) { { bulk_upload:, field_109: "E07000223" } } + + it "sets to 1" do + expect(parser.log.previous_la_known).to eq(1) + end + end + + context "when not known" do + let(:attributes) { { bulk_upload:, field_109: "" } } + + it "sets to 0" do + expect(parser.log.previous_la_known).to eq(0) + end + end + end + + describe "#first_time_property_let_as_social_housing" do + context "when field_27 is 15, 16, or 17" do + let(:attributes) { { bulk_upload:, field_27: %w[15 16 17].sample } } + + it "sets to 1" do + expect(parser.log.first_time_property_let_as_social_housing).to eq(1) + end + end + + context "when field_27 is not 15, 16, or 17" do + let(:attributes) { { bulk_upload:, field_27: "1" } } + + it "sets to 0" do + expect(parser.log.first_time_property_let_as_social_housing).to eq(0) + end + end + end + + describe "#housingneeds" do + context "when no disabled needs" do + let(:attributes) { { bulk_upload:, field_87: "1" } } + + it "sets to 2" do + expect(parser.log.housingneeds).to eq(2) + end + end + + context "when dont know about disabled needs" do + let(:attributes) { { bulk_upload:, field_88: "1" } } + + it "sets to 3" do + expect(parser.log.housingneeds).to eq(3) + end + end + end + + describe "#housingneeds_type" do + context "when field_83 is 1" do + let(:attributes) { { bulk_upload:, field_83: "1" } } + + it "set to 0" do + expect(parser.log.housingneeds_type).to eq(0) + end + end + + context "when field_84 is 1" do + let(:attributes) { { bulk_upload:, field_84: "1" } } + + it "set to 1" do + expect(parser.log.housingneeds_type).to eq(1) + end + end + + context "when field_85 is 1" do + let(:attributes) { { bulk_upload:, field_85: "1" } } + + it "set to 2" do + expect(parser.log.housingneeds_type).to eq(2) + end + end + end + + describe "#housingneeds_other" do + context "when field_58 is 1" do + let(:attributes) { { bulk_upload:, field_86: "1" } } + + it "sets to 1" do + expect(parser.log.housingneeds_other).to eq(1) + end + end + end + end + + describe "#start_date" do + context "when year of 9 is passed to represent 2009" do + let(:attributes) { { bulk_upload:, field_7: "1", field_8: "1", field_9: "9" } } + + it "uses the year 2009" do + expect(parser.send(:start_date)).to eql(Date.new(2009, 1, 1)) + end + end + end +end diff --git a/spec/services/bulk_upload/processor_spec.rb b/spec/services/bulk_upload/processor_spec.rb index 8aaef8c36..2254e1b75 100644 --- a/spec/services/bulk_upload/processor_spec.rb +++ b/spec/services/bulk_upload/processor_spec.rb @@ -21,6 +21,7 @@ RSpec.describe BulkUpload::Processor do BulkUpload::Lettings::Validator, invalid?: true, call: nil, + errors: [], ) end diff --git a/spec/services/bulk_upload/sales/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2022/row_parser_spec.rb similarity index 87% rename from spec/services/bulk_upload/sales/row_parser_spec.rb rename to spec/services/bulk_upload/sales/year2022/row_parser_spec.rb index 38f02fabb..e27355dc4 100644 --- a/spec/services/bulk_upload/sales/row_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2022/row_parser_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe BulkUpload::Sales::RowParser do +RSpec.describe BulkUpload::Sales::Year2022::RowParser do subject(:parser) { described_class.new(attributes) } describe "validations" do diff --git a/spec/services/csv/lettings_log_csv_service_spec.rb b/spec/services/csv/lettings_log_csv_service_spec.rb index 95b3069dc..00955bc37 100644 --- a/spec/services/csv/lettings_log_csv_service_spec.rb +++ b/spec/services/csv/lettings_log_csv_service_spec.rb @@ -222,7 +222,7 @@ RSpec.describe Csv::LettingsLogCsvService do location_mobility_type location_admin_district location_startdate] - csv = CSV.parse(described_class.new(user).to_csv) + csv = CSV.parse(described_class.new(user, export_type: "labels").to_csv) expect(csv.first).to eq(expected_csv_attributes) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c5ea8e451..72083e6a9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -111,8 +111,15 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods - config.before { RequestHelper.stub_http_requests } - config.after { RequestHelper.real_http_requests } + config.before do + RequestHelper.stub_http_requests + Rails.application.load_seed + end + + config.after do + RequestHelper.real_http_requests + Singleton.__init__(FormHandler) + end end RSpec::Matchers.define_negated_matcher :not_change, :change diff --git a/spec/support/bulk_upload/log_to_csv.rb b/spec/support/bulk_upload/log_to_csv.rb index e15c9173b..86167376d 100644 --- a/spec/support/bulk_upload/log_to_csv.rb +++ b/spec/support/bulk_upload/log_to_csv.rb @@ -8,9 +8,49 @@ class BulkUpload::LogToCsv @overrides = overrides end - def to_csv_row + def row_prefix + [nil] * col_offset + end + + def to_2022_csv_row + (row_prefix + to_2022_row).flatten.join(",") + line_ending + end + + def to_2023_csv_row(seed: nil) + if seed + row = to_2023_row.shuffle(random: Random.new(seed)) + (row_prefix + row).flatten.join(",") + line_ending + else + (row_prefix + to_2023_row).flatten.join(",") + line_ending + end + end + + def to_2023_row + to_2022_row + [ + log.needstype, + log.location&.id, + nil, # uprn, + nil, # address_line_1, + nil, # address_line_2, + nil, # town_or_city, + nil, # county, + ] + end + + def default_2023_field_numbers_row(seed: nil) + if seed + ["Bulk upload field number"] + default_2023_field_numbers.shuffle(random: Random.new(seed)) + else + ["Bulk upload field number"] + default_2023_field_numbers + end.flatten.join(",") + line_ending + end + + def default_2023_field_numbers + [5, nil, nil, 15, 16, nil, 13, 40, 41, 42, 43, 46, 52, 56, 60, 64, 68, 72, 76, 47, 53, 57, 61, 65, 69, 73, 77, 51, 55, 59, 63, 67, 71, 75, 50, 54, 58, 62, 66, 70, 74, 78, 48, 49, 79, 81, 82, 123, 124, 122, 120, 102, 103, nil, 83, 84, 85, 86, 87, 88, 104, 109, 107, 108, 106, 100, 101, 105, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 126, 128, 129, 130, 131, 132, 127, 125, 133, 134, 33, 34, 35, 36, 37, 38, nil, 7, 8, 9, 28, 14, 32, 29, 30, 31, 26, 27, 25, 23, 24, nil, 1, 3, 2, 80, nil, 121, 44, 89, 98, 92, 95, 90, 91, 93, 94, 97, 96, 99, 10, 11, 12, 45, 39, 6, 4, 17, 18, 19, 20, 21, 22] + end + + def to_2022_row [ - [nil] * col_offset, # 0 log.renttype, # 1 nil, nil, @@ -155,10 +195,11 @@ class BulkUpload::LogToCsv log.declaration, log.joint, renewal, - line_ending, - ].flatten.join(",") + ] end +private + def renewal checkbox_value(log.renewal) end