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", }, ]) %> + +
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 @@