Browse Source

Merge branch 'main' into CLDC-850-demographic-details-relationship-validation

# Conflicts:
#	app/models/validations/shared_validations.rb
#	db/schema.rb
pull/1178/head
natdeanlewissoftwire 4 years ago
parent
commit
4db70499c9
  1. 33
      app/components/bulk_upload_error_row_component.html.erb
  2. 48
      app/components/bulk_upload_error_row_component.rb
  3. 9
      app/controllers/bulk_upload_lettings_results_controller.rb
  4. 9
      app/controllers/bulk_upload_sales_results_controller.rb
  5. 7
      app/jobs/process_bulk_upload_job.rb
  6. 5
      app/models/bulk_upload.rb
  7. 3
      app/models/bulk_upload_error.rb
  8. 2
      app/models/form.rb
  9. 1
      app/models/form/sales/pages/buyer1_previous_tenure.rb
  10. 18
      app/models/form/sales/pages/previous_property_type.rb
  11. 18
      app/models/form/sales/pages/previous_tenure.rb
  12. 20
      app/models/form/sales/questions/fromprop.rb
  13. 2
      app/models/form/sales/questions/gender_identity1.rb
  14. 20
      app/models/form/sales/questions/previous_tenure.rb
  15. 2
      app/models/form/sales/subsections/property_information.rb
  16. 2
      app/models/form/sales/subsections/shared_ownership_scheme.rb
  17. 15
      app/models/forms/bulk_upload_lettings/upload_your_file.rb
  18. 19
      app/models/forms/bulk_upload_sales/upload_your_file.rb
  19. 1
      app/models/user.rb
  20. 13
      app/models/validations/shared_validations.rb
  21. 18
      app/services/bulk_upload/downloader.rb
  22. 185
      app/services/bulk_upload/lettings/row_parser.rb
  23. 250
      app/services/bulk_upload/lettings/validator.rb
  24. 42
      app/services/bulk_upload/processor.rb
  25. 176
      app/services/bulk_upload/sales/row_parser.rb
  26. 228
      app/services/bulk_upload/sales/validator.rb
  27. 26
      app/services/storage/local_disk_service.rb
  28. 20
      app/views/bulk_upload_lettings_results/show.html.erb
  29. 20
      app/views/bulk_upload_sales_results/show.html.erb
  30. 2
      config/environments/development.rb
  31. 2
      config/environments/test.rb
  32. 24
      config/forms/2021_2022.json
  33. 24
      config/forms/2022_2023.json
  34. 4
      config/initializers/feature_toggle.rb
  35. 3
      config/locales/en.yml
  36. 8
      config/routes.rb
  37. 19
      db/migrate/20221209161927_create_bulk_upload_errors.rb
  38. 5
      db/migrate/20230103100531_rename_purchaser_code.rb
  39. 7
      db/migrate/20230109124623_add_fromprop_to_sales.rb
  40. 7
      db/migrate/20230109140532_add_previous_tenure_to_sales.rb
  41. 5
      db/migrate/20230112093524_remove_illness_type0.rb
  42. 31
      db/schema.rb
  43. 75
      spec/components/bulk_upload_error_row_component_spec.rb
  44. 9
      spec/factories/bulk_upload.rb
  45. 14
      spec/factories/bulk_upload_error.rb
  46. 2
      spec/factories/lettings_log.rb
  47. 2
      spec/factories/sales_log.rb
  48. 3
      spec/features/form/checkboxes_spec.rb
  49. 2
      spec/fixtures/exports/general_needs_log.csv
  50. 4
      spec/fixtures/exports/general_needs_log.xml
  51. 6
      spec/fixtures/exports/supported_housing_logs.xml
  52. 20
      spec/fixtures/files/2021_22_lettings_bulk_upload.csv
  53. 119
      spec/fixtures/files/2022_23_sales_bulk_upload.csv
  54. 4
      spec/fixtures/files/lettings_logs_download.csv
  55. 4
      spec/fixtures/files/lettings_logs_download_non_support.csv
  56. 97
      spec/fixtures/forms/2021_2022.json
  57. 2
      spec/models/form/lettings/questions/location_id_spec.rb
  58. 14
      spec/models/form/question_spec.rb
  59. 2
      spec/models/form/sales/pages/buyer1_previous_tenure_spec.rb
  60. 35
      spec/models/form/sales/pages/previous_property_type_spec.rb
  61. 35
      spec/models/form/sales/pages/previous_tenure_spec.rb
  62. 47
      spec/models/form/sales/questions/fromprop_spec.rb
  63. 2
      spec/models/form/sales/questions/gender_identity1_spec.rb
  64. 47
      spec/models/form/sales/questions/previous_tenure_spec.rb
  65. 2
      spec/models/form/sales/subsections/property_information_spec.rb
  66. 2
      spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb
  67. 4
      spec/models/form_handler_spec.rb
  68. 6
      spec/models/forms/bulk_upload_lettings/upload_your_file_spec.rb
  69. 6
      spec/models/forms/bulk_upload_sales/upload_your_file_spec.rb
  70. 10
      spec/models/lettings_log_spec.rb
  71. 16
      spec/models/validations/financial_validations_spec.rb
  72. 48
      spec/models/validations/shared_validations_spec.rb
  73. 57
      spec/requests/bulk_upload_lettings_results_controller_spec.rb
  74. 70
      spec/requests/bulk_upload_sales_results_controller_spec.rb
  75. 23
      spec/services/bulk_upload/downloader_spec.rb
  76. 81
      spec/services/bulk_upload/lettings/row_parser_spec.rb
  77. 39
      spec/services/bulk_upload/lettings/validator_spec.rb
  78. 34
      spec/services/bulk_upload/processor_spec.rb
  79. 21
      spec/services/bulk_upload/sales/row_parser_spec.rb
  80. 47
      spec/services/bulk_upload/sales/validator_spec.rb
  81. 1
      spec/services/csv/lettings_log_csv_service_spec.rb
  82. 6
      spec/services/exports/lettings_log_export_service_spec.rb

33
app/components/bulk_upload_error_row_component.html.erb

@ -0,0 +1,33 @@
<div class="x-govuk-summary-card govuk-!-margin-bottom-6">
<div class="x-govuk-summary-card__header">
<% if lettings? %>
<h3 class="x-govuk-summary-card__title"><strong>Row <%= row %></strong> <span class="govuk-!-margin-left-3">Tenant code: <%= tenant_code %></span> <span class="govuk-!-margin-left-3">Property reference: <%= property_ref %></span></h3>
<% else %>
<h3 class="x-govuk-summary-card__title"><strong>Row <%= row %></strong> <span class="govuk-!-margin-left-3">Purchaser code: <%= purchaser_code %></span></h3>
<% end %>
</div>
<div class="x-govuk-summary-card__body">
<%= govuk_table do |table| %>
<% table.head do |head| %>
<% head.row do |row| %>
<% row.cell(header: true, text: "Cell") %>
<% row.cell(header: true, text: "Question") %>
<% row.cell(header: true, text: "Error") %>
<% row.cell(header: true, text: "Specification") %>
<% end %>
<% table.body do |body| %>
<% bulk_upload_errors.each do |error| %>
<% body.row do |row| %>
<% row.cell(header: true, text: error.cell) %>
<% row.cell(text: question_for_field(error.field)) %>
<% row.cell(text: error.error) %>
<% row.cell(text: error.field.humanize) %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
</div>

48
app/components/bulk_upload_error_row_component.rb

@ -0,0 +1,48 @@
class BulkUploadErrorRowComponent < ViewComponent::Base
attr_reader :bulk_upload_errors
def initialize(bulk_upload_errors:)
@bulk_upload_errors = bulk_upload_errors
super
end
def row
bulk_upload_errors.first.row
end
def tenant_code
bulk_upload_errors.first.tenant_code
end
def purchaser_code
bulk_upload_errors.first.purchaser_code
end
def property_ref
bulk_upload_errors.first.property_ref
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
end
def bulk_upload
bulk_upload_errors.first.bulk_upload
end
def lettings?
bulk_upload.log_type == "lettings"
end
def sales?
bulk_upload.log_type == "sales"
end
end

9
app/controllers/bulk_upload_lettings_results_controller.rb

@ -0,0 +1,9 @@
class BulkUploadLettingsResultsController < ApplicationController
before_action :authenticate_user!
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
def show
@bulk_upload = current_user.bulk_uploads.lettings.find(params[:id])
end
end

9
app/controllers/bulk_upload_sales_results_controller.rb

@ -0,0 +1,9 @@
class BulkUploadSalesResultsController < ApplicationController
before_action :authenticate_user!
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
def show
@bulk_upload = current_user.bulk_uploads.sales.find(params[:id])
end
end

7
app/jobs/process_bulk_upload_job.rb

@ -0,0 +1,7 @@
class ProcessBulkUploadJob < ApplicationJob
queue_as :default
def perform(bulk_upload:)
BulkUpload::Processor.new(bulk_upload:).call
end
end

5
app/models/bulk_upload.rb

@ -2,9 +2,14 @@ class BulkUpload < ApplicationRecord
enum log_type: { lettings: "lettings", sales: "sales" }
belongs_to :user
has_many :bulk_upload_errors
after_initialize :generate_identifier, unless: :identifier
def year_combo
"#{year}/#{year - 2000 + 1}"
end
private
def generate_identifier

3
app/models/bulk_upload_error.rb

@ -0,0 +1,3 @@
class BulkUploadError < ApplicationRecord
belongs_to :bulk_upload
end

2
app/models/form.rb

@ -167,10 +167,12 @@ class Form
if %w[radio checkbox].include?(question.type)
enabled_answer_options = enabled_question_ids.include?(question.id) ? enabled_questions.find { |q| q.id == question.id }.answer_options : {}
current_answer_option_valid = enabled_answer_options.present? ? enabled_answer_options.key?(log.public_send(question.id).to_s) : false
if !current_answer_option_valid && log.respond_to?(question.id.to_s)
Rails.logger.debug("Cleared #{question.id} value")
log.public_send("#{question.id}=", nil)
else
(question.answer_options.keys - enabled_answer_options.keys).map do |invalid_answer_option|
Rails.logger.debug("Cleared #{invalid_answer_option} value")
log.public_send("#{invalid_answer_option}=", nil) if log.respond_to?(invalid_answer_option)

1
app/models/form/sales/pages/buyer1_previous_tenure.rb

@ -2,7 +2,6 @@ class Form::Sales::Pages::Buyer1PreviousTenure < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "buyer1_previous_tenure"
@header = "What was buyer 1's previous tenure?"
end
def questions

18
app/models/form/sales/pages/previous_property_type.rb

@ -0,0 +1,18 @@
class Form::Sales::Pages::PreviousPropertyType < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "previous_property_type"
@header = ""
@description = ""
@subsection = subsection
@depends_on = [{
"soctenant" => 1,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::Fromprop.new(nil, nil, self),
]
end
end

18
app/models/form/sales/pages/previous_tenure.rb

@ -0,0 +1,18 @@
class Form::Sales::Pages::PreviousTenure < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "shared_ownership_previous_tenure"
@header = ""
@description = ""
@subsection = subsection
@depends_on = [{
"soctenant" => 1,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::PreviousTenure.new(nil, nil, self),
]
end
end

20
app/models/form/sales/questions/fromprop.rb

@ -0,0 +1,20 @@
class Form::Sales::Questions::Fromprop < ::Form::Question
def initialize(id, hsh, page)
super
@id = "fromprop"
@check_answer_label = "Previous property type"
@header = "What was the previous property type?"
@type = "radio"
@hint_text = ""
@page = page
@answer_options = ANSWER_OPTIONS
end
ANSWER_OPTIONS = {
"1" => { "value" => "Flat or maisonette" },
"2" => { "value" => "Bedsit" },
"3" => { "value" => "House" },
"4" => { "value" => "Bungalow" },
"9" => { "value" => "Other" },
}.freeze
end

2
app/models/form/sales/questions/gender_identity1.rb

@ -14,6 +14,6 @@ class Form::Sales::Questions::GenderIdentity1 < ::Form::Question
"F" => { "value" => "Female" },
"M" => { "value" => "Male" },
"X" => { "value" => "Non-binary" },
"R" => { "value" => "Prefers not to say " },
"R" => { "value" => "Prefers not to say" },
}.freeze
end

20
app/models/form/sales/questions/previous_tenure.rb

@ -0,0 +1,20 @@
class Form::Sales::Questions::PreviousTenure < ::Form::Question
def initialize(id, hsh, page)
super
@id = "socprevten"
@check_answer_label = "Previous property tenure"
@header = "What was the previous tenure of the buyer?"
@type = "radio"
@hint_text = ""
@page = page
@answer_options = ANSWER_OPTIONS
end
ANSWER_OPTIONS = {
"1" => { "value" => "Social Rent" },
"2" => { "value" => "Affordable Rent" },
"3" => { "value" => "London Affordable Rent" },
"9" => { "value" => "Other" },
"10" => { "value" => "Don’t know" },
}.freeze
end

2
app/models/form/sales/subsections/property_information.rb

@ -9,8 +9,8 @@ class Form::Sales::Subsections::PropertyInformation < ::Form::Subsection
def pages
@pages ||= [
Form::Sales::Pages::PropertyNumberOfBedrooms.new(nil, nil, self),
Form::Sales::Pages::PropertyBuildingType.new(nil, nil, self),
Form::Sales::Pages::PropertyUnitType.new(nil, nil, self),
Form::Sales::Pages::PropertyBuildingType.new(nil, nil, self),
Form::Sales::Pages::PropertyLocalAuthority.new(nil, nil, self),
Form::Sales::Pages::PropertyWheelchairAccessible.new(nil, nil, self),
]

2
app/models/form/sales/subsections/shared_ownership_scheme.rb

@ -17,6 +17,8 @@ class Form::Sales::Subsections::SharedOwnershipScheme < ::Form::Subsection
Form::Sales::Pages::LaNominations.new(nil, nil, self),
Form::Sales::Pages::BuyerPrevious.new(nil, nil, self),
Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self),
Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self),
Form::Sales::Pages::PreviousTenure.new(nil, nil, self),
Form::Sales::Pages::AboutPriceSharedOwnership.new(nil, nil, self),
Form::Sales::Pages::Mortgageused.new("mortgage_used_shared_ownership", nil, self),
Form::Sales::Pages::MortgageAmount.new("mortgage_amount_shared_ownership", nil, self),

15
app/models/forms/bulk_upload_lettings/upload_your_file.rb

@ -41,9 +41,9 @@ module Forms
filename: file.original_filename,
)
if upload_enabled?
storage_service.write_file(bulk_upload.identifier, File.read(file.path))
end
storage_service.write_file(bulk_upload.identifier, File.read(file.path))
ProcessBulkUploadJob.perform_later(bulk_upload:)
true
end
@ -55,7 +55,14 @@ module Forms
end
def storage_service
@storage_service ||= Storage::S3Service.new(Configuration::PaasConfigurationService.new, ENV["CSV_DOWNLOAD_PAAS_INSTANCE"])
@storage_service ||= if upload_enabled?
Storage::S3Service.new(
Configuration::PaasConfigurationService.new,
ENV["CSV_DOWNLOAD_PAAS_INSTANCE"],
)
else
Storage::LocalDiskService.new
end
end
def validate_file_is_csv

19
app/models/forms/bulk_upload_sales/upload_your_file.rb

@ -38,21 +38,24 @@ module Forms
filename: file.original_filename,
)
if upload_enabled?
storage_service.write_file(bulk_upload.identifier, File.read(file.path))
end
storage_service.write_file(bulk_upload.identifier, File.read(file.path))
ProcessBulkUploadJob.perform_later(bulk_upload:)
true
end
private
def upload_enabled?
!Rails.env.development?
end
def storage_service
@storage_service ||= Storage::S3Service.new(Configuration::PaasConfigurationService.new, ENV["CSV_DOWNLOAD_PAAS_INSTANCE"])
@storage_service ||= if FeatureToggle.upload_enabled?
Storage::S3Service.new(
Configuration::PaasConfigurationService.new,
ENV["CSV_DOWNLOAD_PAAS_INSTANCE"],
)
else
Storage::LocalDiskService.new
end
end
def validate_file_is_csv

1
app/models/user.rb

@ -12,6 +12,7 @@ class User < ApplicationRecord
has_many :owned_sales_logs, through: :organisation
has_many :managed_sales_logs, through: :organisation
has_many :legacy_users
has_many :bulk_uploads
validates :name, presence: true
validates :email, presence: true

13
app/models/validations/shared_validations.rb

@ -69,6 +69,19 @@ module Validations::SharedValidations
{ scope: status, date: date&.to_formatted_s(:govuk_date), deactivation_date: closest_reactivation&.deactivation_date&.to_formatted_s(:govuk_date) }
end
def validate_valid_radio_option(record)
return unless FeatureToggle.validate_valid_radio_options?
record.attributes.each do |question_id, _v|
question = record.form.get_question(question_id, record)
next unless question&.type == "radio"
next unless record[question_id].present? && !question.answer_options.key?(record[question_id].to_s) && question.page.routed_to?(record, nil)
record.errors.add(question_id, I18n.t("validations.invalid_option", question: question.check_answer_label&.downcase))
end
end
def shared_validate_household_number_of_other_members(record, max_people)
(2..max_people).each do |n|
shared_validate_person_age_matches_economic_status(record, n)

18
app/services/bulk_upload/downloader.rb

@ -11,6 +11,10 @@ class BulkUpload::Downloader
download
end
def delete_local_file!
file.unlink
end
private
def download
@ -25,9 +29,21 @@ private
end
def storage_service
@storage_service ||= Storage::S3Service.new(
@storage_service ||= if FeatureToggle.upload_enabled?
s3_storage_service
else
local_disk_storage_service
end
end
def s3_storage_service
Storage::S3Service.new(
Configuration::PaasConfigurationService.new,
ENV["CSV_DOWNLOAD_PAAS_INSTANCE"],
)
end
def local_disk_storage_service
Storage::LocalDiskService.new
end
end

185
app/services/bulk_upload/lettings/row_parser.rb

@ -0,0 +1,185 @@
class BulkUpload::Lettings::RowParser
include ActiveModel::Model
include ActiveModel::Attributes
attribute :field_1, :integer
attribute :field_2
attribute :field_3
attribute :field_4, :integer
attribute :field_5, :integer
attribute :field_6
attribute :field_7, :string
attribute :field_8, :integer
attribute :field_9, :integer
attribute :field_10, :string
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, :string
attribute :field_27, :string
attribute :field_28, :string
attribute :field_29, :string
attribute :field_30, :string
attribute :field_31, :string
attribute :field_32, :string
attribute :field_33, :string
attribute :field_34, :string
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, :integer
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
attribute :field_55, :integer
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, :string
attribute :field_63, :string
attribute :field_64, :string
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, :integer
attribute :field_75, :integer
attribute :field_76, :integer
attribute :field_77, :integer
attribute :field_78, :integer
attribute :field_79, :integer
attribute :field_80, :decimal
attribute :field_81, :decimal
attribute :field_82, :decimal
attribute :field_83, :decimal
attribute :field_84, :decimal
attribute :field_85, :decimal
attribute :field_86, :integer
attribute :field_87, :integer
attribute :field_88, :decimal
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
attribute :field_96, :integer
attribute :field_97, :integer
attribute :field_98, :integer
attribute :field_99, :integer
attribute :field_100, :string
attribute :field_101, :integer
attribute :field_102, :integer
attribute :field_103, :integer
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
attribute :field_111, :integer
attribute :field_112, :string
attribute :field_113, :integer
attribute :field_114, :integer
attribute :field_115
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
attribute :field_126, :integer
attribute :field_127, :integer
attribute :field_128, :integer
attribute :field_129, :integer
attribute :field_130, :integer
attribute :field_131, :string
attribute :field_132, :integer
attribute :field_133, :integer
attribute :field_134, :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 ||= LettingsLog.new(attributes_for_log)
end
def field_mapping
{
field_134: :renewal,
}
end
def validate_possible_answers
field_mapping.each do |field, attribute|
possible_answers = FormHandler.instance.current_lettings_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

250
app/services/bulk_upload/lettings/validator.rb

@ -0,0 +1,250 @@
require "csv"
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_max_columns
def initialize(bulk_upload:, path:)
@bulk_upload = bulk_upload
@path = path
end
def call
row_offset = 6
col_offset = 0
row_parsers.each_with_index do |row_parser, index|
row_parser.valid?
row = index + row_offset
row_parser.errors.each do |error|
bulk_upload.bulk_upload_errors.create!(
field: error.attribute,
error: error.type,
tenant_code: row_parser.field_7,
property_ref: row_parser.field_100,
row:,
cell: "#{cols[field_number_for_attribute(error.attribute) + col_offset]}#{row}",
)
end
end
end
def self.question_for_field(field)
QUESTIONS[field]
end
private
def field_number_for_attribute(attribute)
attribute.to_s.split("_").last.to_i
end
def cols
@cols ||= ("A".."EE").to_a
end
def row_parsers
@row_parsers ||= body_rows.map do |row|
stripped_row = row[1..]
headers = ("field_1".."field_134").to_a
hash = Hash[headers.zip(stripped_row)]
BulkUpload::Lettings::RowParser.new(hash)
end
end
# determine the row seperator from CSV
# Windows will use \r\n
def row_sep
contents = ""
File.open(path, "r") do |f|
f.seek(9900)
contents = f.read
end
rn_count = contents.scan("\r\n").count
n_count = contents.scan(/[^\r]\n/).count
if rn_count > n_count
"\r\n"
else
"\n"
end
end
def rows
@rows ||= CSV.read(path, row_sep:)
end
def body_rows
rows[6..]
end
def validate_file_not_empty
if File.size(path).zero?
errors.add(:file, :blank)
halt_validations!
end
end
def validate_max_columns
return if halt_validations?
max_row_size = rows.map(&:size).max
errors.add(:file, :max_row_size) if max_row_size > 136
end
def halt_validations!
@halt_validations = true
end
def halt_validations?
@halt_validations ||= false
end
end

42
app/services/bulk_upload/processor.rb

@ -0,0 +1,42 @@
class BulkUpload::Processor
attr_reader :bulk_upload
def initialize(bulk_upload:)
@bulk_upload = bulk_upload
end
def call
download
validator.call
ensure
downloader.delete_local_file!
end
private
def downloader
@downloader ||= BulkUpload::Downloader.new(bulk_upload:)
end
def download
downloader.call
end
def validator
@validator ||= validator_class.new(
bulk_upload:,
path: downloader.path,
)
end
def validator_class
case bulk_upload.log_type
when "lettings"
BulkUpload::Lettings::Validator
when "sales"
BulkUpload::Sales::Validator
else
raise "Validator not found for #{bulk_upload.log_type}"
end
end
end

176
app/services/bulk_upload/sales/row_parser.rb

@ -0,0 +1,176 @@
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

228
app/services/bulk_upload/sales/validator.rb

@ -0,0 +1,228 @@
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
validate :validate_max_columns
def initialize(bulk_upload:, path:)
@bulk_upload = bulk_upload
@path = path
end
def call
row_parsers.each_with_index do |row_parser, index|
row_parser.valid?
row = index + row_offset + 1
row_parser.errors.each do |error|
bulk_upload.bulk_upload_errors.create!(
field: error.attribute,
error: error.type,
purchaser_code: row_parser.field_1,
row:,
cell: "#{cols[field_number_for_attribute(error.attribute) + col_offset - 1]}#{row}",
)
end
end
end
private
def field_number_for_attribute(attribute)
attribute.to_s.split("_").last.to_i
end
def rows
@rows ||= CSV.read(path, row_sep:)
end
def body_rows
rows[row_offset..]
end
def row_offset
5
end
def col_offset
1
end
def cols
@cols ||= ("A".."DV").to_a
end
def row_parsers
@row_parsers ||= body_rows.map do |row|
stripped_row = row[col_offset..]
headers = ("field_1".."field_125").to_a
hash = Hash[headers.zip(stripped_row)]
BulkUpload::Sales::RowParser.new(hash)
end
end
def row_sep
"\r\n"
# "\n"
end
def validate_file_not_empty
if File.size(path).zero?
errors.add(:file, :blank)
halt_validations!
end
end
def validate_max_columns
return if halt_validations?
max_row_size = rows.map(&:size).max
errors.add(:file, :max_row_size) if max_row_size > 126
end
def halt_validations!
@halt_validations = true
end
def halt_validations?
@halt_validations ||= false
end
end

26
app/services/storage/local_disk_service.rb

@ -0,0 +1,26 @@
require "fileutils"
module Storage
class LocalDiskService < StorageService
def list_files(folder = "/")
path = Rails.root.join("tmp/storage", folder)
Dir.entries(path)
end
def get_file_io(filename)
path = Rails.root.join("tmp/storage", filename)
File.open(path, "r")
end
def write_file(filename, data)
path = Rails.root.join("tmp/storage", filename)
FileUtils.mkdir_p(path.dirname)
File.open(path, "w") do |f|
f.write data
end
end
end
end

20
app/views/bulk_upload_lettings_results/show.html.erb

@ -0,0 +1,20 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<span class="govuk-caption-l">Bulk Upload for lettings (<%= @bulk_upload.year_combo %>)</span>
<h1 class="govuk-heading-l">We found <%= pluralize(@bulk_upload.bulk_upload_errors.count, "error") %> in your file</h1>
<div class="govuk-body">
Here’s a list of everything that you need to fix your spreadsheet. You can download the specification to help you fix the cells in your CSV file.
</div>
<h2 class="govuk-heading-m"><%= @bulk_upload.filename %></h2>
</div>
</div>
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
<% @bulk_upload.bulk_upload_errors.group_by(&:row).each do |_row, errors_for_row| %>
<%= render BulkUploadErrorRowComponent.new(bulk_upload_errors: errors_for_row) %>
<% end %>
</div>
</div>

20
app/views/bulk_upload_sales_results/show.html.erb

@ -0,0 +1,20 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<span class="govuk-caption-l">Bulk Upload for sales (<%= @bulk_upload.year_combo %>)</span>
<h1 class="govuk-heading-l">We found <%= pluralize(@bulk_upload.bulk_upload_errors.count, "error") %> in your file</h1>
<div class="govuk-body">
Here’s a list of everything that you need to fix your spreadsheet. You can download the specification to help you fix the cells in your CSV file.
</div>
<h2 class="govuk-heading-m"><%= @bulk_upload.filename %></h2>
</div>
</div>
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
<% @bulk_upload.bulk_upload_errors.group_by(&:row).each do |_row, errors_for_row| %>
<%= render BulkUploadErrorRowComponent.new(bulk_upload_errors: errors_for_row) %>
<% end %>
</div>
</div>

2
config/environments/development.rb

@ -83,4 +83,6 @@ Rails.application.configure do
# see https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017
config.active_record.yaml_column_permitted_classes = [Time]
config.active_job.queue_adapter = :inline
end

2
config/environments/test.rb

@ -65,4 +65,6 @@ Rails.application.configure do
# see https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017
config.active_record.yaml_column_permitted_classes = [Time]
config.active_job.queue_adapter = :test
end

24
config/forms/2021_2022.json

@ -758,14 +758,6 @@
"renewal": 0,
"rsnvac": 13
},
{
"renewal": 0,
"rsnvac": 16
},
{
"renewal": 0,
"rsnvac": 17
},
{
"renewal": 0,
"rsnvac": 18
@ -827,6 +819,14 @@
{
"renewal": 0,
"rsnvac": 15
},
{
"renewal": 0,
"rsnvac": 16
},
{
"renewal": 0,
"rsnvac": 17
}
]
},
@ -893,14 +893,6 @@
"renewal": 0,
"rsnvac": 13
},
{
"renewal": 0,
"rsnvac": 16
},
{
"renewal": 0,
"rsnvac": 17
},
{
"renewal": 0,
"rsnvac": 18

24
config/forms/2022_2023.json

@ -758,14 +758,6 @@
"renewal": 0,
"rsnvac": 13
},
{
"renewal": 0,
"rsnvac": 16
},
{
"renewal": 0,
"rsnvac": 17
},
{
"renewal": 0,
"rsnvac": 18
@ -827,6 +819,14 @@
{
"renewal": 0,
"rsnvac": 15
},
{
"renewal": 0,
"rsnvac": 16
},
{
"renewal": 0,
"rsnvac": 17
}
]
},
@ -893,14 +893,6 @@
"renewal": 0,
"rsnvac": 13
},
{
"renewal": 0,
"rsnvac": 16
},
{
"renewal": 0,
"rsnvac": 17
},
{
"renewal": 0,
"rsnvac": 18

4
config/initializers/feature_toggle.rb

@ -30,4 +30,8 @@ class FeatureToggle
def self.upload_enabled?
!Rails.env.development?
end
def self.validate_valid_radio_options?
!(Rails.env.production? || Rails.env.staging?)
end
end

3
config/locales/en.yml

@ -128,6 +128,7 @@ en:
blank: "You must choose a managing agent"
already_added: "You have already added this managing agent"
not_answered: "You must answer %{question}"
invalid_option: "Enter a valid value for %{question}"
other_field_missing: "If %{main_field_label} is other then %{other_field_label} must be provided"
other_field_not_required: "%{other_field_label} must not be provided if %{main_field_label} was not other"
@ -373,8 +374,6 @@ en:
owning_organisation:
does_not_own_stock: "Enter an organisation that owns housing stock"
location:
postcode_blank: "Enter a postcode"
units: "The units at this location must be a number"

8
config/routes.rb

@ -123,15 +123,19 @@ Rails.application.routes.draw do
collection do
post "bulk-upload", to: "bulk_upload#bulk_upload"
get "bulk-upload", to: "bulk_upload#show"
get "csv-download", to: "lettings_logs#download_csv"
post "email-csv", to: "lettings_logs#email_csv"
get "csv-confirmation", to: "lettings_logs#csv_confirmation"
resources :bulk_upload_lettings_logs, path: "bulk-upload-logs" do
resources :bulk_upload_lettings_logs, path: "bulk-upload-logs", only: %i[show update] do
collection do
get :start
end
end
resources :bulk_upload_lettings_results, path: "bulk-upload-results", only: [:show]
get "update-logs", to: "lettings_logs#update_logs"
end
@ -158,6 +162,8 @@ Rails.application.routes.draw do
get :start
end
end
resources :bulk_upload_sales_results, path: "bulk-upload-results", only: [:show]
end
FormHandler.instance.sales_forms.each do |_key, form|

19
db/migrate/20221209161927_create_bulk_upload_errors.rb

@ -0,0 +1,19 @@
class CreateBulkUploadErrors < ActiveRecord::Migration[7.0]
def change
create_table :bulk_upload_errors do |t|
t.references :bulk_upload
t.text :cell
t.text :row
t.text :tenant_code
t.text :property_ref
t.text :purchase_code
t.text :field
t.text :error
t.timestamps
end
end
end

5
db/migrate/20230103100531_rename_purchaser_code.rb

@ -0,0 +1,5 @@
class RenamePurchaserCode < ActiveRecord::Migration[7.0]
def change
rename_column :bulk_upload_errors, :purchase_code, :purchaser_code
end
end

7
db/migrate/20230109124623_add_fromprop_to_sales.rb

@ -0,0 +1,7 @@
class AddFrompropToSales < ActiveRecord::Migration[7.0]
def change
change_table :sales_logs, bulk: true do |t|
t.column :fromprop, :integer
end
end
end

7
db/migrate/20230109140532_add_previous_tenure_to_sales.rb

@ -0,0 +1,7 @@
class AddPreviousTenureToSales < ActiveRecord::Migration[7.0]
def change
change_table :sales_logs, bulk: true do |t|
t.column :socprevten, :integer
end
end
end

5
db/migrate/20230112093524_remove_illness_type0.rb

@ -0,0 +1,5 @@
class RemoveIllnessType0 < ActiveRecord::Migration[7.0]
def change
remove_column :lettings_logs, :illness_type_0, :integer
end
end

31
db/schema.rb

@ -10,10 +10,24 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_01_09_160738) do
ActiveRecord::Schema[7.0].define(version: 2023_01_12_093524) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "bulk_upload_errors", force: :cascade do |t|
t.bigint "bulk_upload_id"
t.text "cell"
t.text "row"
t.text "tenant_code"
t.text "property_ref"
t.text "purchaser_code"
t.text "field"
t.text "error"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["bulk_upload_id"], name: "index_bulk_upload_errors_on_bulk_upload_id"
end
create_table "bulk_uploads", force: :cascade do |t|
t.bigint "user_id"
t.text "log_type", null: false
@ -236,7 +250,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_01_09_160738) do
t.string "old_id"
t.integer "joint"
t.bigint "created_by_id"
t.integer "illness_type_0"
t.integer "retirement_value_check"
t.integer "tshortfall_known"
t.integer "sheltered"
@ -421,9 +434,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_01_09_160738) do
t.string "relat5"
t.string "relat6"
t.integer "hb"
t.string "sex4"
t.string "sex5"
t.string "sex6"
t.integer "savings_value_check"
t.integer "deposit_value_check"
t.integer "frombeds"
@ -460,17 +470,22 @@ ActiveRecord::Schema[7.0].define(version: 2023_01_09_160738) do
t.integer "hhregres"
t.integer "hhregresstill"
t.integer "proplen"
t.integer "mscharge_known"
t.decimal "mscharge", precision: 10, scale: 2
t.integer "prevten"
t.integer "mortgageused"
t.integer "wchair"
t.integer "armedforcesspouse"
t.integer "mscharge_known"
t.decimal "mscharge", precision: 10, scale: 2
t.string "sex4"
t.string "sex5"
t.string "sex6"
t.integer "mortlen"
t.datetime "hodate", precision: nil
t.integer "hoday"
t.integer "homonth"
t.integer "hoyear"
t.integer "mortlen"
t.integer "fromprop"
t.integer "socprevten"
t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id"
t.index ["managing_organisation_id"], name: "index_sales_logs_on_managing_organisation_id"
t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id"

75
spec/components/bulk_upload_error_row_component_spec.rb

@ -0,0 +1,75 @@
require "rails_helper"
RSpec.describe BulkUploadErrorRowComponent, type: :component do
context "when a single error" do
let(:row) { rand(9_999) }
let(:tenant_code) { SecureRandom.hex(4) }
let(:property_ref) { SecureRandom.hex(4) }
let(:field) { :field_134 }
let(:error) { "some error" }
let(:bulk_upload) { create(:bulk_upload, :lettings) }
let(:bulk_upload_errors) do
[
FactoryBot.build(
:bulk_upload_error,
bulk_upload:,
row:,
tenant_code:,
property_ref:,
field:,
error:,
),
]
end
it "renders the row number" do
result = render_inline(described_class.new(bulk_upload_errors:))
expect(result).to have_content("Row #{row}")
end
it "renders the tenant_code" do
result = render_inline(described_class.new(bulk_upload_errors:))
expect(result).to have_content("Tenant code: #{tenant_code}")
end
it "renders the property_ref" do
result = render_inline(described_class.new(bulk_upload_errors:))
expect(result).to have_content("Property reference: #{property_ref}")
end
it "renders the cell of error" do
expected = bulk_upload_errors.first.cell
result = render_inline(described_class.new(bulk_upload_errors:))
expect(result).to have_content(expected)
end
it "renders the question for lettings" do
expected = "Is this letting a renewal?"
result = render_inline(described_class.new(bulk_upload_errors:))
expect(result).to have_content(expected)
end
context "when a sales bulk upload" do
let(:bulk_upload) { create(:bulk_upload, :sales) }
let(:field) { :field_87 }
it "renders the question for sales" do
expected = "What is the full purchase price?"
result = render_inline(described_class.new(bulk_upload_errors:))
expect(result).to have_content(expected)
end
end
it "renders the error" do
expected = error
result = render_inline(described_class.new(bulk_upload_errors:))
expect(result).to have_content(expected)
end
it "renders the field number" do
expected = bulk_upload_errors.first.field.humanize
result = render_inline(described_class.new(bulk_upload_errors:))
expect(result).to have_content(expected)
end
end
end

9
spec/factories/bulk_upload.rb

@ -6,5 +6,14 @@ FactoryBot.define do
log_type { BulkUpload.log_types.values.sample }
year { 2022 }
identifier { SecureRandom.uuid }
sequence(:filename) { |n| "bulk-upload-#{n}.csv" }
trait(:sales) do
log_type { BulkUpload.log_types[:sales] }
end
trait(:lettings) do
log_type { BulkUpload.log_types[:lettings] }
end
end
end

14
spec/factories/bulk_upload_error.rb

@ -0,0 +1,14 @@
require "securerandom"
FactoryBot.define do
factory :bulk_upload_error do
bulk_upload
row { rand(9_999) }
cell { "#{('A'..'Z').to_a.sample}#{row}" }
tenant_code { SecureRandom.hex(4) }
property_ref { SecureRandom.hex(4) }
purchaser_code { SecureRandom.hex(4) }
field { "field_#{rand(134)}" }
error { "some error" }
end
end

2
spec/factories/lettings_log.rb

@ -83,7 +83,7 @@ FactoryBot.define do
postcode_known { 1 }
postcode_full { Faker::Address.postcode }
reasonpref { 1 }
cbl { 1 }
cbl { 0 }
chr { 1 }
cap { 0 }
reasonother { nil }

2
spec/factories/sales_log.rb

@ -32,7 +32,7 @@ FactoryBot.define do
age2 { 35 }
builtype { 1 }
ethnic { 3 }
ethnic_group { 12 }
ethnic_group { 17 }
sex2 { "X" }
buy2livein { "1" }
ecstat1 { "1" }

3
spec/features/form/checkboxes_spec.rb

@ -41,7 +41,8 @@ RSpec.describe "Checkboxes" do
context "when a checkbox question is submitted with invalid answers" do
before do
lettings_log.update!(illness: 100)
lettings_log.illness = 100
lettings_log.save!(validate: false)
allow(lettings_log).to receive(:update).and_return(false)
end

2
spec/fixtures/exports/general_needs_log.csv vendored

@ -1,2 +1,2 @@
status,tenancycode,age1,sex1,ethnic,national,prevten,ecstat1,hhmemb,age2,sex2,ecstat2,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,leftreg,reservist,illness,preg_occ,startertenancy,tenancylength,tenancy,ppostcode_full,rsnvac,unittype_gn,beds,offered,wchair,earnings,incfreq,benefits,period,layear,waityear,postcode_full,reasonpref,cbl,chr,cap,reasonother,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_f,housingneeds_g,housingneeds_h,illness_type_1,illness_type_2,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,irproduct_other,reason,propcode,la,prevloc,hb,hbrentshortfall,mrcdate,incref,startdate,armedforces,unitletas,builtype,voiddate,renttype,needstype,lettype,totchild,totelder,totadult,nocharge,referral,brent,scharge,pscharge,supcharg,tcharge,tshortfall,chcharge,ppcodenk,has_benefits,renewal,wrent,wscharge,wpschrge,wsupchrg,wtcharge,wtshortfall,refused,housingneeds,wchchrg,newprop,relat2,relat3,relat4,relat5,relat6,relat7,relat8,lar,irproduct,joint,sheltered,hhtype,new_old,vacdays,form,owningorgid,owningorgname,hcnum,maningorgid,maningorgname,manhcnum,createddate,uploaddate
2,BZ737,35,F,2,4,6,0,2,32,M,6,,,,,,,,,,,,,,,,,,,1,0,1,1,1,2,1,5,1,SE2 6RT,6,7,3,2,1,68,1,1,2,2,1,NW1 5TY,1,1,1,2,,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,,,4,123,E09000003,E07000105,6,1,2020-05-05T10:36:49+01:00,0,2022-02-02T10:36:49+00:00,1,2,1,2019-11-03T00:00:00+00:00,2,1,7,0,0,2,0,2,200.0,50.0,40.0,35.0,325.0,12.0,,1,1,0,100.0,25.0,20.0,17.5,162.5,6.0,0,1,,2,P,,,,,,,,,,,4,2,638,{id},{owning_org_id},DLUHC,1234,{managing_org_id},DLUHC,1234,2022-02-08T16:52:15+00:00,2022-02-08T16:52:15+00:00
2,BZ737,35,F,2,4,6,0,2,32,M,6,,,,,,,,,,,,,,,,,,,1,4,1,1,1,2,1,5,1,SE2 6RT,6,7,3,2,1,68,1,1,2,2,1,NW1 5TY,1,2,1,2,,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,,,4,123,E09000003,E07000105,6,1,2020-05-05T10:36:49+01:00,0,2022-02-02T10:36:49+00:00,1,2,1,2019-11-03T00:00:00+00:00,2,1,7,0,0,2,0,2,200.0,50.0,40.0,35.0,325.0,12.0,,1,1,0,100.0,25.0,20.0,17.5,162.5,6.0,0,1,,2,P,,,,,,,,,,,4,2,638,{id},{owning_org_id},DLUHC,1234,{managing_org_id},DLUHC,1234,2022-02-08T16:52:15+00:00,2022-02-08T16:52:15+00:00

1 status tenancycode age1 sex1 ethnic national prevten ecstat1 hhmemb age2 sex2 ecstat2 age3 sex3 ecstat3 age4 sex4 ecstat4 age5 sex5 ecstat5 age6 sex6 ecstat6 age7 sex7 ecstat7 age8 sex8 ecstat8 homeless underoccupation_benefitcap leftreg reservist illness preg_occ startertenancy tenancylength tenancy ppostcode_full rsnvac unittype_gn beds offered wchair earnings incfreq benefits period layear waityear postcode_full reasonpref cbl chr cap reasonother housingneeds_a housingneeds_b housingneeds_c housingneeds_f housingneeds_g housingneeds_h illness_type_1 illness_type_2 illness_type_3 illness_type_4 illness_type_8 illness_type_5 illness_type_6 illness_type_7 illness_type_9 illness_type_10 rp_homeless rp_insan_unsat rp_medwel rp_hardship rp_dontknow tenancyother irproduct_other reason propcode la prevloc hb hbrentshortfall mrcdate incref startdate armedforces unitletas builtype voiddate renttype needstype lettype totchild totelder totadult nocharge referral brent scharge pscharge supcharg tcharge tshortfall chcharge ppcodenk has_benefits renewal wrent wscharge wpschrge wsupchrg wtcharge wtshortfall refused housingneeds wchchrg newprop relat2 relat3 relat4 relat5 relat6 relat7 relat8 lar irproduct joint sheltered hhtype new_old vacdays form owningorgid owningorgname hcnum maningorgid maningorgname manhcnum createddate uploaddate
2 2 BZ737 35 F 2 4 6 0 2 32 M 6 1 0 4 1 1 1 2 1 5 1 SE2 6RT 6 7 3 2 1 68 1 1 2 2 1 NW1 5TY 1 1 2 1 2 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 4 123 E09000003 E07000105 6 1 2020-05-05T10:36:49+01:00 0 2022-02-02T10:36:49+00:00 1 2 1 2019-11-03T00:00:00+00:00 2 1 7 0 0 2 0 2 200.0 50.0 40.0 35.0 325.0 12.0 1 1 0 100.0 25.0 20.0 17.5 162.5 6.0 0 1 2 P 4 2 638 {id} {owning_org_id} DLUHC 1234 {managing_org_id} DLUHC 1234 2022-02-08T16:52:15+00:00 2022-02-08T16:52:15+00:00

4
spec/fixtures/exports/general_needs_log.xml vendored

@ -32,7 +32,7 @@
<sex8/>
<ecstat8/>
<homeless>1</homeless>
<underoccupation_benefitcap>0</underoccupation_benefitcap>
<underoccupation_benefitcap>4</underoccupation_benefitcap>
<leftreg>1</leftreg>
<reservist>1</reservist>
<illness>1</illness>
@ -54,7 +54,7 @@
<waityear>1</waityear>
<postcode_full>NW1 5TY</postcode_full>
<reasonpref>1</reasonpref>
<cbl>1</cbl>
<cbl>2</cbl>
<chr>1</chr>
<cap>2</cap>
<reasonother/>

6
spec/fixtures/exports/supported_housing_logs.xml vendored

@ -32,7 +32,7 @@
<sex8/>
<ecstat8/>
<homeless>1</homeless>
<underoccupation_benefitcap>0</underoccupation_benefitcap>
<underoccupation_benefitcap>4</underoccupation_benefitcap>
<leftreg>1</leftreg>
<reservist>1</reservist>
<illness>1</illness>
@ -53,7 +53,7 @@
<waityear>1</waityear>
<postcode_full>SW1A 2AA</postcode_full>
<reasonpref>1</reasonpref>
<cbl>1</cbl>
<cbl>2</cbl>
<chr>1</chr>
<cap>2</cap>
<reasonother/>
@ -130,7 +130,7 @@
<relat8/>
<lar/>
<irproduct/>
<sheltered>0</sheltered>
<sheltered>1</sheltered>
<hhtype>4</hhtype>
<new_old>2</new_old>
<vacdays>638</vacdays>

20
spec/fixtures/files/2021_22_lettings_bulk_upload.csv vendored

File diff suppressed because one or more lines are too long

119
spec/fixtures/files/2022_23_sales_bulk_upload.csv vendored

@ -0,0 +1,119 @@
Question,What is the purchaser code?,What is the day of the sale completion date? - DD,What is the month of the sale completion date? - MM,What is the year of the sale completion date? - YY,[BLANK],Was the buyer interviewed for any of the answers you will provide on this log?,Age of Buyer 1,Age of Buyer 2 or Person 2,Age of Person 3,Age of Person 4,Age of Person 5,Age of Person 6,Gender identity of Buyer 1,Gender identity of Buyer 2 or Person 2,Gender identity of Person 3,Gender identity of Person 4,Gender identity of Person 5,Gender identity of Person 6,Person 2's relationship to lead tenant,Person 3's relationship to lead tenant,Person 4's relationship to lead tenant,Person 5's relationship to lead tenant,Person 6's relationship to lead tenant,Working situation of Buyer 1,Working situation of Buyer 2 or Person 2,Working situation of Person 3,Working situation of Person 4,Working situation of Person 5,Working situation of Person 6,What is the buyer 1's ethnic group?,What is buyer 1's nationality?,What is buyer 1's gross annual income?,What is buyer 2's gross annual income?,Was buyer 1's income used for a mortgage application?,Was buyer 2's income used for a mortgage application?,"What is the total amount the buyers had in savings before they paid any deposit for the property?
To the nearest £10",Have any of the buyers previously owned a property?,[BLANK],What was buyer 1's previous tenure?,What is the local authority of buyer 1's last settled home,Part 1 of postcode of buyer 1's last settled home,Part 2 of postcode of buyer 1's last settled home,Do you know the postcode of buyer 1's last settled home?,Was the buyer registered with their PRP (HA)?,Was the buyer registered with the local authority?,Was the buyer registered with a Help to Buy agent?,Was the buyer registered with another PRP (HA)?,Does anyone in the household consider themselves to have a disability?,Does anyone in the household use a wheelchair?,How many bedrooms does the property have?,What type of unit is the property?,Which type of building is the property?,What is the local authority of the property?,Part 1 of postcode of property,Part 2 of postcode of property,Is the property built or adapted to wheelchair-user standards?,What is the type of shared ownership sale?,"Is this a resale?
Shared ownership","What is the day of the practical completion or handover date? - DD
Shared ownership","What is the month of the practical completion or handover date? - MM
Shared ownership","What is the year of the practical completion or handover date? - YY
Shared ownership","What is the day of the exchange of contracts date? - DD
Shared ownership","What is the month of the exchange of contracts date? - MM
Shared ownership","What is the year of the exchange of contracts date? - YY
Shared ownership","Was the household re-housed under a local authority nominations agreement?
Shared ownership","How many bedrooms did the buyer's previous property have?
Shared ownership","What was the type of the buyer's previous property?
Shared ownership","What was the full purchase price?
Shared ownership","What was the initial percentage equity stake purchased?
Shared ownership","What is the mortgage amount?
Shared ownership","Does this include any extra borrowing?
Shared ownership","How much was the cash deposit paid on the property?
Shared ownership","How much cash discount was given through Social Homebuy?
Shared ownership","What is the basic monthly rent?
Shared ownership","What are the total monthly leasehold charges for the property?
Shared ownership",What is the type of discounted ownership sale?,"What was the full purchase price?
Discounted ownership","What was the amount of any loan, grant, discount or subsidy given?
Discounted ownership","What was the percentage discount?
Discounted ownership","What is the mortgage amount?
Discounted ownership","Does this include any extra borrowing?
Discounted ownership","How much was the cash deposit paid on the property?
Discounted ownership","What are the total monthly leasehold charges for the property?
Discounted ownership",What is the type of outright sale?,"What is the 'other' type of outright sale?
Outright sale",[BLANK],"What is the full purchase price?
Outright sale","What is the mortgage amount?
Outright sale","Does this include any extra borrowing?
Outright sale","How much was the cash deposit paid on the property?
Outright sale","What are the total monthly leasehold charges for the property?
Outright sale","Which organisation owned this property before the sale?
Organisation's CORE ID",Username,BLANK,Has the buyer ever served in the UK Armed Forces and for how long?,[BLANK],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?,"What is the name of the mortgage lender?
Shared ownership","What is the name of the 'other' mortgage lender?
Shared ownership","What is the name of the mortgage lender?
Discounted ownership","What is the name of the 'other' mortgage lender?
Discounted ownership","What is the name of the mortgage lender?
Outright sale","What is the name of the 'other' mortgage lender?
Outright sale",Were the buyers receiving any of these housing-related benefits immediately before buying this property?,"What is the length of the mortgage in years?
Shared ownership","What is the length of the mortgage in years?
Discounted ownership","What is the length of the mortgage in years?
Outright sale","How long have the buyers been living in the property before the purchase?
Discounted ownership",Are there more than two joint purchasers of this property?,"How long have the buyers been living in the property before the purchase?
Shared ownership",Is this a staircasing transaction?,Data Protection question,Was this purchase made through an ownership scheme?,"Is the buyer a company?
Outright sale",Will the buyers live in the property?,Is this a joint purchase?,Will buyer 1 live in the property?,Will buyer 2 live in the property?,"Besides the buyers, how many people live in the property?","What percentage of the property has been bought in this staircasing transaction?
Shared ownership","What percentage of the property does the buyer now own in total?
Shared ownership","What was the rent type of the buyer's previous property?
Shared ownership","Was a mortgage used for the purchase of this property?
Shared ownership","Was a mortgage used for the purchase of this property?
Discounted ownership","Was a mortgage used for the purchase of this property?
Outright sale"
Values,Max 9 digits,1 - 31,1 - 12,19 - 23,,1 or null,"15 - 110
or R",1 - 110 or R,,,,,"M, F, X or R",,,,,,"P, C, X or R",,,,,0 - 10,,,,,,1 - 19,"12 -13, 17 -19",0 - 99999,,1 or 2,1 or 2,0 - 999990,1 - 3,,1 - 7 or 9,ONS CODE - E + 9 digits,XXX(X),XXX,1 or null,,,,,1 - 3,1 - 3,1 - 9,1 - 4 or 9,1 or 2,ONS CODE E + 9 digits,XXX(X),XXX,1 - 3,"2, 16, 18, 24, 28 or 30-31",1 or 2,1 - 31,1 - 12,19 - 23,1 - 31,1 - 12,19 - 23,1 - 3,1 - 9,1 - 4 or 9,0 - 999999,0 - 100,0 - 999999,1 - 3,0 - 999999,,0 - 999.99,,"8, 9, 14, 21, 22, 27 or 29",0 - 999999,,0 - 100,0 - 999999,1 - 3,0 - 999999,0 - 999.99,10 or 12,,,0 - 999999,,1-3,0 - 999999,0-999.99,Up to 7 digits,Username of CORE account this sales log should be assigned to,,3 - 8,,4 - 7,1 - 40,,1 - 40,,1 - 40,,1 - 4, Integer <=60, Integer <=60, Integer <=60, Integer <=80,1 - 3, Integer <=80,1 - 3,1,1 - 3,1 - 2,1 - 2,1 - 2,1 - 2,1 - 2,0 - 5,1 - 100,1 - 100,1-3 or 9-10,1 - 2,1 - 2,1 - 2
Can be Null?,No,,,,,No,No,"If fields 14, 19 and 25 are all also null","If fields 15, 20 and 26 are all also null","If fields 16, 21 and 27 are all also null","If fields 17, 22 and 28 are all also null","If fields 18, 23 and 29 are all also null",No,"If fields 8, 19 and 25 are all also null","If fields 9, 20 and 26 are also null","If fields 10, 21 and 27 are all also null","If fields 11, 22 and 28 are all also null","If fields 12, 23 and 29 are all also null","If fields 8, 14 and 25 are all also null","If fields 9, 15 and 26 are all also null","If fields 10, 16 and 27 are all also null","If fields 11, 17 and 28 are all also null","If fields 12, 18 and 29 are all also null",If field 6 = 1,"If fields 8, 14 and 19 are all also null","If fields 9, 15 and 20 are all also null","If fields 10, 16 and 21 are all also null","If fields 11, 17 and 22 are all also null","If fields 12, 18 and 23 are all also null",If field 6 = 1,,,If field 116 = 2,If field 32 is null,If field 116 = 2,If field 6 = 1,,,If field 6 = 1,No,If field 43 = 1,,If fields 41 and 42 BOTH have valid entries,Yes,,,,If field 6 = 1,,No,,,,,,,If field 113 = 2 or 3,,,,,,,,,"If field 113 = 2 or 3
OR
field 39 = 3 - 7 or 9",,If field 113 = 2 or 3,,,,,"If field 57 is null, 2, 16, 24 or 28",If field 113 = 2 or 3,,If field 113 = 1 or 3,If field 76 is null,"If field 76 is null, 9 or 14","If field 76 is null, 8, 21 or 22",If field 113 = 1 or 3,,,,If field 113 = 1 or 2,If field 84 is null or 10,,If field 113 = 1 or 2,,,,,No,Yes,,No,,No,If field 113 = 2 or 3,"If field 113 = 2 or 3
OR
If field 98 is not 40",If field 113 = 1 or 3,"If field 113 = 1 or 3
OR
If field 100 is not 40",If field 113 = 1 or 2,"If field 113 = 1 or 2
OR
If field 102 is not 40",No,If field 113 = 2 or 3,If field 113 = 1 or 3,If field 113 = 1 or 2,If field 113 = 1 or 3,If field 116 = 2,If field 113 = 2 or 3,If field 113 = 2 or 3,No,No,If field 113 = 1 or 2,If field 113 = 1 or 2,No,No,If field 116 = 2,No,If field 113 = 2 or 3,If field 113 = 2 or 3,"If field 113 = 1 or 2
OR
If field 39 = 3 - 9",If field 113 = 2 or 3,If field 113 = 1 or 3,If field 113 = 1 or 2
Bulk upload format and duplicate check,Yes,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Field number,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125
,1,1,1,23,,1,30,,,,,,M,,,,,,,,,,,1,,,,,,1,18,20000,,1,,10000,2,,3,,EC1N,2TD,2,2,2,2,2,2,2,2,1,1,,,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1 Question What is the purchaser code? What is the day of the sale completion date? - DD What is the month of the sale completion date? - MM What is the year of the sale completion date? - YY [BLANK] Was the buyer interviewed for any of the answers you will provide on this log? Age of Buyer 1 Age of Buyer 2 or Person 2 Age of Person 3 Age of Person 4 Age of Person 5 Age of Person 6 Gender identity of Buyer 1 Gender identity of Buyer 2 or Person 2 Gender identity of Person 3 Gender identity of Person 4 Gender identity of Person 5 Gender identity of Person 6 Person 2's relationship to lead tenant Person 3's relationship to lead tenant Person 4's relationship to lead tenant Person 5's relationship to lead tenant Person 6's relationship to lead tenant Working situation of Buyer 1 Working situation of Buyer 2 or Person 2 Working situation of Person 3 Working situation of Person 4 Working situation of Person 5 Working situation of Person 6 What is the buyer 1's ethnic group? What is buyer 1's nationality? What is buyer 1's gross annual income? What is buyer 2's gross annual income? Was buyer 1's income used for a mortgage application? Was buyer 2's income used for a mortgage application? What is the total amount the buyers had in savings before they paid any deposit for the property? To the nearest £10 Have any of the buyers previously owned a property? [BLANK] What was buyer 1's previous tenure? What is the local authority of buyer 1's last settled home Part 1 of postcode of buyer 1's last settled home Part 2 of postcode of buyer 1's last settled home Do you know the postcode of buyer 1's last settled home? Was the buyer registered with their PRP (HA)? Was the buyer registered with the local authority? Was the buyer registered with a Help to Buy agent? Was the buyer registered with another PRP (HA)? Does anyone in the household consider themselves to have a disability? Does anyone in the household use a wheelchair? How many bedrooms does the property have? What type of unit is the property? Which type of building is the property? What is the local authority of the property? Part 1 of postcode of property Part 2 of postcode of property Is the property built or adapted to wheelchair-user standards? What is the type of shared ownership sale? Is this a resale? Shared ownership What is the day of the practical completion or handover date? - DD Shared ownership What is the month of the practical completion or handover date? - MM Shared ownership What is the year of the practical completion or handover date? - YY Shared ownership What is the day of the exchange of contracts date? - DD Shared ownership What is the month of the exchange of contracts date? - MM Shared ownership What is the year of the exchange of contracts date? - YY Shared ownership Was the household re-housed under a local authority nominations agreement? Shared ownership How many bedrooms did the buyer's previous property have? Shared ownership What was the type of the buyer's previous property? Shared ownership What was the full purchase price? Shared ownership What was the initial percentage equity stake purchased? Shared ownership What is the mortgage amount? Shared ownership Does this include any extra borrowing? Shared ownership How much was the cash deposit paid on the property? Shared ownership How much cash discount was given through Social Homebuy? Shared ownership What is the basic monthly rent? Shared ownership What are the total monthly leasehold charges for the property? Shared ownership What is the type of discounted ownership sale? What was the full purchase price? Discounted ownership What was the amount of any loan, grant, discount or subsidy given? Discounted ownership What was the percentage discount? Discounted ownership What is the mortgage amount? Discounted ownership Does this include any extra borrowing? Discounted ownership How much was the cash deposit paid on the property? Discounted ownership What are the total monthly leasehold charges for the property? Discounted ownership What is the type of outright sale? What is the 'other' type of outright sale? Outright sale [BLANK] What is the full purchase price? Outright sale What is the mortgage amount? Outright sale Does this include any extra borrowing? Outright sale How much was the cash deposit paid on the property? Outright sale What are the total monthly leasehold charges for the property? Outright sale Which organisation owned this property before the sale? Organisation's CORE ID Username BLANK Has the buyer ever served in the UK Armed Forces and for how long? [BLANK] 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? What is the name of the mortgage lender? Shared ownership What is the name of the 'other' mortgage lender? Shared ownership What is the name of the mortgage lender? Discounted ownership What is the name of the 'other' mortgage lender? Discounted ownership What is the name of the mortgage lender? Outright sale What is the name of the 'other' mortgage lender? Outright sale Were the buyers receiving any of these housing-related benefits immediately before buying this property? What is the length of the mortgage in years? Shared ownership What is the length of the mortgage in years? Discounted ownership What is the length of the mortgage in years? Outright sale How long have the buyers been living in the property before the purchase? Discounted ownership Are there more than two joint purchasers of this property? How long have the buyers been living in the property before the purchase? Shared ownership Is this a staircasing transaction? Data Protection question Was this purchase made through an ownership scheme? Is the buyer a company? Outright sale Will the buyers live in the property? Is this a joint purchase? Will buyer 1 live in the property? Will buyer 2 live in the property? Besides the buyers, how many people live in the property? What percentage of the property has been bought in this staircasing transaction? Shared ownership What percentage of the property does the buyer now own in total? Shared ownership What was the rent type of the buyer's previous property? Shared ownership Was a mortgage used for the purchase of this property? Shared ownership Was a mortgage used for the purchase of this property? Discounted ownership Was a mortgage used for the purchase of this property? Outright sale
2 Values Max 9 digits 1 - 31 1 - 12 19 - 23 1 or null 15 - 110 or R 1 - 110 or R M, F, X or R P, C, X or R 0 - 10 1 - 19 12 -13, 17 -19 0 - 99999 1 or 2 1 or 2 0 - 999990 1 - 3 1 - 7 or 9 ONS CODE - E + 9 digits XXX(X) XXX 1 or null 1 - 3 1 - 3 1 - 9 1 - 4 or 9 1 or 2 ONS CODE E + 9 digits XXX(X) XXX 1 - 3 2, 16, 18, 24, 28 or 30-31 1 or 2 1 - 31 1 - 12 19 - 23 1 - 31 1 - 12 19 - 23 1 - 3 1 - 9 1 - 4 or 9 0 - 999999 0 - 100 0 - 999999 1 - 3 0 - 999999 0 - 999.99 8, 9, 14, 21, 22, 27 or 29 0 - 999999 0 - 100 0 - 999999 1 - 3 0 - 999999 0 - 999.99 10 or 12 0 - 999999 1-3 0 - 999999 0-999.99 Up to 7 digits Username of CORE account this sales log should be assigned to 3 - 8 4 - 7 1 - 40 1 - 40 1 - 40 1 - 4 Integer <=60 Integer <=60 Integer <=60 Integer <=80 1 - 3 Integer <=80 1 - 3 1 1 - 3 1 - 2 1 - 2 1 - 2 1 - 2 1 - 2 0 - 5 1 - 100 1 - 100 1-3 or 9-10 1 - 2 1 - 2 1 - 2
3 Can be Null? No No No If fields 14, 19 and 25 are all also null If fields 15, 20 and 26 are all also null If fields 16, 21 and 27 are all also null If fields 17, 22 and 28 are all also null If fields 18, 23 and 29 are all also null No If fields 8, 19 and 25 are all also null If fields 9, 20 and 26 are also null If fields 10, 21 and 27 are all also null If fields 11, 22 and 28 are all also null If fields 12, 23 and 29 are all also null If fields 8, 14 and 25 are all also null If fields 9, 15 and 26 are all also null If fields 10, 16 and 27 are all also null If fields 11, 17 and 28 are all also null If fields 12, 18 and 29 are all also null If field 6 = 1 If fields 8, 14 and 19 are all also null If fields 9, 15 and 20 are all also null If fields 10, 16 and 21 are all also null If fields 11, 17 and 22 are all also null If fields 12, 18 and 23 are all also null If field 6 = 1 If field 116 = 2 If field 32 is null If field 116 = 2 If field 6 = 1 If field 6 = 1 No If field 43 = 1 If fields 41 and 42 BOTH have valid entries Yes If field 6 = 1 No If field 113 = 2 or 3 If field 113 = 2 or 3 OR field 39 = 3 - 7 or 9 If field 113 = 2 or 3 If field 57 is null, 2, 16, 24 or 28 If field 113 = 2 or 3 If field 113 = 1 or 3 If field 76 is null If field 76 is null, 9 or 14 If field 76 is null, 8, 21 or 22 If field 113 = 1 or 3 If field 113 = 1 or 2 If field 84 is null or 10 If field 113 = 1 or 2 No Yes No No If field 113 = 2 or 3 If field 113 = 2 or 3 OR If field 98 is not 40 If field 113 = 1 or 3 If field 113 = 1 or 3 OR If field 100 is not 40 If field 113 = 1 or 2 If field 113 = 1 or 2 OR If field 102 is not 40 No If field 113 = 2 or 3 If field 113 = 1 or 3 If field 113 = 1 or 2 If field 113 = 1 or 3 If field 116 = 2 If field 113 = 2 or 3 If field 113 = 2 or 3 No No If field 113 = 1 or 2 If field 113 = 1 or 2 No No If field 116 = 2 No If field 113 = 2 or 3 If field 113 = 2 or 3 If field 113 = 1 or 2 OR If field 39 = 3 - 9 If field 113 = 2 or 3 If field 113 = 1 or 3 If field 113 = 1 or 2
4 Bulk upload format and duplicate check Yes
5 Field number 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
6 1 1 1 23 1 30 M 1 1 18 20000 1 10000 2 3 EC1N 2TD 2 2 2 2 2 2 2 2 1 1 2

4
spec/fixtures/files/lettings_logs_download.csv vendored

@ -1,2 +1,2 @@
id,status,created_at,updated_at,created_by_name,is_dpo,owning_organisation_name,managing_organisation_name,collection_start_year,needstype,renewal,startdate,rent_type_detail,irproduct_other,tenancycode,propcode,age1,sex1,ecstat1,hhmemb,relat2,age2,sex2,retirement_value_check,ecstat2,armedforces,leftreg,illness,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_h,is_previous_la_inferred,prevloc_label,prevloc,illness_type_1,illness_type_2,is_la_inferred,la_label,la,postcode_known,postcode_full,previous_la_known,wchair,preg_occ,cbl,earnings,incfreq,net_income_value_check,benefits,hb,period,brent,scharge,pscharge,supcharg,tcharge,offered,layear,ppostcode_full,mrcdate,declaration,ethnic,national,prevten,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,reservist,startertenancy,tenancylength,tenancy,rsnvac,unittype_gn,beds,waityear,reasonpref,chr,cap,reasonother,housingneeds_f,housingneeds_g,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,property_owner_organisation,property_manager_organisation,purchaser_code,reason,majorrepairs,hbrentshortfall,property_relet,incref,first_time_property_let_as_social_housing,unitletas,builtype,voiddate,renttype,lettype,totchild,totelder,totadult,net_income_known,nocharge,is_carehome,household_charge,referral,tshortfall,chcharge,ppcodenk,age1_known,age2_known,age3_known,age4_known,age5_known,age6_known,age7_known,age8_known,ethnic_group,letting_allocation_unknown,details_known_2,details_known_3,details_known_4,details_known_5,details_known_6,details_known_7,details_known_8,has_benefits,wrent,wscharge,wpschrge,wsupchrg,wtcharge,wtshortfall,refused,housingneeds,wchchrg,newprop,relat3,relat4,relat5,relat6,relat7,relat8,rent_value_check,old_form_id,lar,irproduct,old_id,joint,illness_type_0,tshortfall_known,sheltered,pregnancy_value_check,hhtype,new_old,vacdays,major_repairs_date_value_check,void_date_value_check,housingneeds_type,housingneeds_other,unresolved,updated_by_id,unittype_sh,scheme_code,scheme_service_name,scheme_sensitive,scheme_type,scheme_registered_under_care_act,scheme_owning_organisation_name,scheme_managing_organisation_name,scheme_primary_client_group,scheme_has_other_client_group,scheme_secondary_client_group,scheme_support_type,scheme_intended_stay,scheme_created_at,location_code,location_postcode,location_name,location_units,location_type_of_unit,location_mobility_type,location_admin_district,location_startdate
{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,No,DLUHC,DLUHC,2021,Supported housing,,2 October 2021,London Affordable Rent,,,,,,,,,,,,,,,,,,,,No,,,,,No,Westminster,E09000033,,SE1 1TE,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,2,8,0,0,0,,0,,,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,0,,,,,,,,,,,,,,,,,,,,9,1,,,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},Missing,No,DLUHC,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2021-04-01 00:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,Bungalow,Fitted with equipment and adaptations,Westminster,{location_startdate}
id,status,created_at,updated_at,created_by_name,is_dpo,owning_organisation_name,managing_organisation_name,collection_start_year,needstype,renewal,startdate,rent_type_detail,irproduct_other,tenancycode,propcode,age1,sex1,ecstat1,hhmemb,relat2,age2,sex2,retirement_value_check,ecstat2,armedforces,leftreg,illness,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_h,is_previous_la_inferred,prevloc_label,prevloc,illness_type_1,illness_type_2,is_la_inferred,la_label,la,postcode_known,postcode_full,previous_la_known,wchair,preg_occ,cbl,earnings,incfreq,net_income_value_check,benefits,hb,period,brent,scharge,pscharge,supcharg,tcharge,offered,layear,ppostcode_full,mrcdate,declaration,ethnic,national,prevten,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,reservist,startertenancy,tenancylength,tenancy,rsnvac,unittype_gn,beds,waityear,reasonpref,chr,cap,reasonother,housingneeds_f,housingneeds_g,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,property_owner_organisation,property_manager_organisation,purchaser_code,reason,majorrepairs,hbrentshortfall,property_relet,incref,first_time_property_let_as_social_housing,unitletas,builtype,voiddate,renttype,lettype,totchild,totelder,totadult,net_income_known,nocharge,is_carehome,household_charge,referral,tshortfall,chcharge,ppcodenk,age1_known,age2_known,age3_known,age4_known,age5_known,age6_known,age7_known,age8_known,ethnic_group,letting_allocation_unknown,details_known_2,details_known_3,details_known_4,details_known_5,details_known_6,details_known_7,details_known_8,has_benefits,wrent,wscharge,wpschrge,wsupchrg,wtcharge,wtshortfall,refused,housingneeds,wchchrg,newprop,relat3,relat4,relat5,relat6,relat7,relat8,rent_value_check,old_form_id,lar,irproduct,old_id,joint,tshortfall_known,sheltered,pregnancy_value_check,hhtype,new_old,vacdays,major_repairs_date_value_check,void_date_value_check,housingneeds_type,housingneeds_other,unresolved,updated_by_id,unittype_sh,scheme_code,scheme_service_name,scheme_sensitive,scheme_type,scheme_registered_under_care_act,scheme_owning_organisation_name,scheme_managing_organisation_name,scheme_primary_client_group,scheme_has_other_client_group,scheme_secondary_client_group,scheme_support_type,scheme_intended_stay,scheme_created_at,location_code,location_postcode,location_name,location_units,location_type_of_unit,location_mobility_type,location_admin_district,location_startdate
{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,No,DLUHC,DLUHC,2021,Supported housing,,2 October 2021,London Affordable Rent,,,,,,,,,,,,,,,,,,,,No,,,,,No,Westminster,E09000033,,SE1 1TE,,No,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,2,8,0,0,0,,0,,,,,,,,,,,,,,,,,,,,,,,,0,,,,,,,0,,,,,,,,,,,,,,,,,,,9,1,,,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},Missing,No,DLUHC,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2021-04-01 00:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,Bungalow,Fitted with equipment and adaptations,Westminster,{location_startdate}

1 id status created_at updated_at created_by_name is_dpo owning_organisation_name managing_organisation_name collection_start_year needstype renewal startdate rent_type_detail irproduct_other tenancycode propcode age1 sex1 ecstat1 hhmemb relat2 age2 sex2 retirement_value_check ecstat2 armedforces leftreg illness housingneeds_a housingneeds_b housingneeds_c housingneeds_h is_previous_la_inferred prevloc_label prevloc illness_type_1 illness_type_2 is_la_inferred la_label la postcode_known postcode_full previous_la_known wchair preg_occ cbl earnings incfreq net_income_value_check benefits hb period brent scharge pscharge supcharg tcharge offered layear ppostcode_full mrcdate declaration ethnic national prevten age3 sex3 ecstat3 age4 sex4 ecstat4 age5 sex5 ecstat5 age6 sex6 ecstat6 age7 sex7 ecstat7 age8 sex8 ecstat8 homeless underoccupation_benefitcap reservist startertenancy tenancylength tenancy rsnvac unittype_gn beds waityear reasonpref chr cap reasonother housingneeds_f housingneeds_g illness_type_3 illness_type_4 illness_type_8 illness_type_5 illness_type_6 illness_type_7 illness_type_9 illness_type_10 rp_homeless rp_insan_unsat rp_medwel rp_hardship rp_dontknow tenancyother property_owner_organisation property_manager_organisation purchaser_code reason majorrepairs hbrentshortfall property_relet incref first_time_property_let_as_social_housing unitletas builtype voiddate renttype lettype totchild totelder totadult net_income_known nocharge is_carehome household_charge referral tshortfall chcharge ppcodenk age1_known age2_known age3_known age4_known age5_known age6_known age7_known age8_known ethnic_group letting_allocation_unknown details_known_2 details_known_3 details_known_4 details_known_5 details_known_6 details_known_7 details_known_8 has_benefits wrent wscharge wpschrge wsupchrg wtcharge wtshortfall refused housingneeds wchchrg newprop relat3 relat4 relat5 relat6 relat7 relat8 rent_value_check old_form_id lar irproduct old_id joint illness_type_0 tshortfall_known sheltered pregnancy_value_check hhtype new_old vacdays major_repairs_date_value_check void_date_value_check housingneeds_type housingneeds_other unresolved updated_by_id unittype_sh scheme_code scheme_service_name scheme_sensitive scheme_type scheme_registered_under_care_act scheme_owning_organisation_name scheme_managing_organisation_name scheme_primary_client_group scheme_has_other_client_group scheme_secondary_client_group scheme_support_type scheme_intended_stay scheme_created_at location_code location_postcode location_name location_units location_type_of_unit location_mobility_type location_admin_district location_startdate
2 {id} in_progress 2022-02-08 16:52:15 +0000 2022-02-08 16:52:15 +0000 Danny Rojas No DLUHC DLUHC 2021 Supported housing 2 October 2021 London Affordable Rent No No Westminster E09000033 SE1 1TE 2 No 2 8 0 0 0 0 0 0 9 1 6 {scheme_code} {scheme_service_name} {scheme_sensitive} Missing No DLUHC DLUHC {scheme_primary_client_group} {scheme_secondary_client_group} {scheme_support_type} {scheme_intended_stay} 2021-04-01 00:00:00 +0100 {location_code} SE1 1TE Downing Street 20 Bungalow Fitted with equipment and adaptations Westminster {location_startdate}

4
spec/fixtures/files/lettings_logs_download_non_support.csv vendored

@ -1,2 +1,2 @@
id,status,created_at,updated_at,created_by_name,is_dpo,owning_organisation_name,managing_organisation_name,collection_start_year,renewal,startdate,irproduct_other,tenancycode,propcode,age1,sex1,ecstat1,relat2,age2,sex2,ecstat2,armedforces,leftreg,illness,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_h,prevloc_label,illness_type_1,illness_type_2,la_label,postcode_full,wchair,preg_occ,cbl,earnings,incfreq,benefits,hb,period,brent,scharge,pscharge,supcharg,tcharge,offered,layear,ppostcode_full,mrcdate,declaration,ethnic,national,prevten,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,reservist,startertenancy,tenancylength,tenancy,rsnvac,unittype_gn,beds,waityear,reasonpref,chr,cap,reasonother,housingneeds_f,housingneeds_g,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,property_owner_organisation,property_manager_organisation,purchaser_code,reason,majorrepairs,hbrentshortfall,property_relet,incref,unitletas,builtype,voiddate,lettype,nocharge,household_charge,referral,tshortfall,chcharge,ppcodenk,ethnic_group,has_benefits,refused,housingneeds,wchchrg,newprop,relat3,relat4,relat5,relat6,relat7,relat8,lar,irproduct,joint,illness_type_0,sheltered,major_repairs_date_value_check,void_date_value_check,housingneeds_type,housingneeds_other,unittype_sh,scheme_code,scheme_service_name,scheme_sensitive,scheme_type,scheme_registered_under_care_act,scheme_owning_organisation_name,scheme_managing_organisation_name,scheme_primary_client_group,scheme_has_other_client_group,scheme_secondary_client_group,scheme_support_type,scheme_intended_stay,scheme_created_at,location_code,location_postcode,location_name,location_units,location_type_of_unit,location_mobility_type,location_admin_district,location_startdate
{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,No,DLUHC,DLUHC,2021,,2 October 2021,,,,,,,,,,,,,,,,,,,,,Westminster,SE1 1TE,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,8,0,,,,,,,0,0,,,,,,,,,,,,,,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},Missing,No,DLUHC,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2021-04-01 00:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,Bungalow,Fitted with equipment and adaptations,Westminster,{location_startdate}
id,status,created_at,updated_at,created_by_name,is_dpo,owning_organisation_name,managing_organisation_name,collection_start_year,renewal,startdate,irproduct_other,tenancycode,propcode,age1,sex1,ecstat1,relat2,age2,sex2,ecstat2,armedforces,leftreg,illness,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_h,prevloc_label,illness_type_1,illness_type_2,la_label,postcode_full,wchair,preg_occ,cbl,earnings,incfreq,benefits,hb,period,brent,scharge,pscharge,supcharg,tcharge,offered,layear,ppostcode_full,mrcdate,declaration,ethnic,national,prevten,age3,sex3,ecstat3,age4,sex4,ecstat4,age5,sex5,ecstat5,age6,sex6,ecstat6,age7,sex7,ecstat7,age8,sex8,ecstat8,homeless,underoccupation_benefitcap,reservist,startertenancy,tenancylength,tenancy,rsnvac,unittype_gn,beds,waityear,reasonpref,chr,cap,reasonother,housingneeds_f,housingneeds_g,illness_type_3,illness_type_4,illness_type_8,illness_type_5,illness_type_6,illness_type_7,illness_type_9,illness_type_10,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,tenancyother,property_owner_organisation,property_manager_organisation,purchaser_code,reason,majorrepairs,hbrentshortfall,property_relet,incref,unitletas,builtype,voiddate,lettype,nocharge,household_charge,referral,tshortfall,chcharge,ppcodenk,ethnic_group,has_benefits,refused,housingneeds,wchchrg,newprop,relat3,relat4,relat5,relat6,relat7,relat8,lar,irproduct,joint,sheltered,major_repairs_date_value_check,void_date_value_check,housingneeds_type,housingneeds_other,unittype_sh,scheme_code,scheme_service_name,scheme_sensitive,scheme_type,scheme_registered_under_care_act,scheme_owning_organisation_name,scheme_managing_organisation_name,scheme_primary_client_group,scheme_has_other_client_group,scheme_secondary_client_group,scheme_support_type,scheme_intended_stay,scheme_created_at,location_code,location_postcode,location_name,location_units,location_type_of_unit,location_mobility_type,location_admin_district,location_startdate
{id},in_progress,2022-02-08 16:52:15 +0000,2022-02-08 16:52:15 +0000,Danny Rojas,No,DLUHC,DLUHC,2021,,2 October 2021,,,,,,,,,,,,,,,,,,,,,Westminster,SE1 1TE,No,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,8,0,,,,,,,0,0,,,,,,,,,,,,,,,,,,6,{scheme_code},{scheme_service_name},{scheme_sensitive},Missing,No,DLUHC,DLUHC,{scheme_primary_client_group},,{scheme_secondary_client_group},{scheme_support_type},{scheme_intended_stay},2021-04-01 00:00:00 +0100,{location_code},SE1 1TE,Downing Street,20,Bungalow,Fitted with equipment and adaptations,Westminster,{location_startdate}

1 id status created_at updated_at created_by_name is_dpo owning_organisation_name managing_organisation_name collection_start_year renewal startdate irproduct_other tenancycode propcode age1 sex1 ecstat1 relat2 age2 sex2 ecstat2 armedforces leftreg illness housingneeds_a housingneeds_b housingneeds_c housingneeds_h prevloc_label illness_type_1 illness_type_2 la_label postcode_full wchair preg_occ cbl earnings incfreq benefits hb period brent scharge pscharge supcharg tcharge offered layear ppostcode_full mrcdate declaration ethnic national prevten age3 sex3 ecstat3 age4 sex4 ecstat4 age5 sex5 ecstat5 age6 sex6 ecstat6 age7 sex7 ecstat7 age8 sex8 ecstat8 homeless underoccupation_benefitcap reservist startertenancy tenancylength tenancy rsnvac unittype_gn beds waityear reasonpref chr cap reasonother housingneeds_f housingneeds_g illness_type_3 illness_type_4 illness_type_8 illness_type_5 illness_type_6 illness_type_7 illness_type_9 illness_type_10 rp_homeless rp_insan_unsat rp_medwel rp_hardship rp_dontknow tenancyother property_owner_organisation property_manager_organisation purchaser_code reason majorrepairs hbrentshortfall property_relet incref unitletas builtype voiddate lettype nocharge household_charge referral tshortfall chcharge ppcodenk ethnic_group has_benefits refused housingneeds wchchrg newprop relat3 relat4 relat5 relat6 relat7 relat8 lar irproduct joint illness_type_0 sheltered major_repairs_date_value_check void_date_value_check housingneeds_type housingneeds_other unittype_sh scheme_code scheme_service_name scheme_sensitive scheme_type scheme_registered_under_care_act scheme_owning_organisation_name scheme_managing_organisation_name scheme_primary_client_group scheme_has_other_client_group scheme_secondary_client_group scheme_support_type scheme_intended_stay scheme_created_at location_code location_postcode location_name location_units location_type_of_unit location_mobility_type location_admin_district location_startdate
2 {id} in_progress 2022-02-08 16:52:15 +0000 2022-02-08 16:52:15 +0000 Danny Rojas No DLUHC DLUHC 2021 2 October 2021 Westminster SE1 1TE 2 No 8 0 0 0 6 {scheme_code} {scheme_service_name} {scheme_sensitive} Missing No DLUHC DLUHC {scheme_primary_client_group} {scheme_secondary_client_group} {scheme_support_type} {scheme_intended_stay} 2021-04-01 00:00:00 +0100 {location_code} SE1 1TE Downing Street 20 Bungalow Fitted with equipment and adaptations Westminster {location_startdate}

97
spec/fixtures/forms/2021_2022.json vendored

@ -152,11 +152,21 @@
"header": "What is person 2’s relationship to lead tenant",
"type": "radio",
"answer_options": {
"P": {
"value": "Partner"
},
"C": {
"value": "Child",
"hint": "Must be eligible for child benefit, aged under 16 or under 20 if still in full-time education."
},
"X": {
"value": "Other"
},
"divider": {
"value": true
},
"R": {
"value": "Prefer not to say"
"value": "Person prefers not to say"
}
}
},
@ -228,8 +238,29 @@
"header": "Which of these best describes person 2’s working situation?",
"type": "radio",
"answer_options": {
"0": {
"value": "Other"
"2": {
"value": "Part-time – Less than 30 hours"
},
"1": {
"value": "Full-time – 30 hours or more"
},
"7": {
"value": "Full-time student"
},
"3": {
"value": "In government training into work, such as New Deal"
},
"4": {
"value": "Jobseeker"
},
"6": {
"value": "Not seeking work"
},
"8": {
"value": "Unable to work because of long term sick or disability"
},
"5": {
"value": "Retired"
},
"9": {
"value": "Child under 16",
@ -245,8 +276,14 @@
}
]
},
"1": {
"value": "Prefer not to say"
"0": {
"value": "Other"
},
"divider": {
"value": true
},
"10": {
"value": "Tenant prefers not to say"
}
}
}
@ -530,10 +567,10 @@
"header": "Is the property built or adapted to wheelchair-user standards?",
"type": "radio",
"answer_options": {
"0": {
"1": {
"value": "Yes"
},
"1": {
"2": {
"value": "No"
}
}
@ -756,11 +793,26 @@
"header": "Is the tenant likely to be in receipt of any of these housing-related benefits?",
"type": "radio",
"answer_options": {
"0": {
"1": {
"value": "Housing benefit"
},
"1": {
"value": "Tenant prefers not to say"
"6": {
"value": "Universal Credit with housing element (excluding housing benefit)"
},
"8": {
"value": "Housing benefit and Universal Credit (without housing element)"
},
"7": {
"value": "Universal Credit (without housing element)"
},
"9": {
"value": "None"
},
"divider": {
"value": true
},
"3": {
"value": "Don’t know"
}
},
"conditional_for": {
@ -818,11 +870,32 @@
"header": "Which period are rent and other charges due?",
"type": "radio",
"answer_options": {
"1": {
"value": "Weekly for 52 weeks"
"2": {
"value": "Every 2 weeks"
},
"3": {
"value": "Every 4 weeks"
},
"4": {
"value": "Every calendar month"
},
"5": {
"value": "Weekly for 50 weeks"
},
"6": {
"value": "Weekly for 49 weeks"
},
"7": {
"value": "Weekly for 48 weeks"
},
"8": {
"value": "Weekly for 47 weeks"
},
"9": {
"value": "Weekly for 46 weeks"
},
"1": {
"value": "Weekly for 52 weeks"
}
}
},

2
spec/models/form/lettings/questions/location_id_spec.rb

@ -39,7 +39,7 @@ RSpec.describe Form::Lettings::Questions::LocationId, type: :model do
context "when getting available locations" do
let(:scheme) { FactoryBot.create(:scheme) }
let(:lettings_log) { FactoryBot.create(:lettings_log, owning_organisation: scheme.owning_organisation, scheme:, needstype: 2) }
let!(:lettings_log) { FactoryBot.create(:lettings_log, owning_organisation: scheme.owning_organisation, scheme:, needstype: 2) }
context "when there are no locations" do
it "the displayed_answer_options is an empty hash" do

14
spec/models/form/question_spec.rb

@ -133,7 +133,19 @@ RSpec.describe Form::Question, type: :model do
let(:page_id) { "person_2_working_situation" }
let(:question_id) { "ecstat2" }
let(:expected_answer_options) do
{ "0" => { "value" => "Other" }, "1" => { "value" => "Prefer not to say" } }
{
"0" => { "value" => "Other" },
"1" => { "value" => "Full-time – 30 hours or more" },
"10" => { "value" => "Tenant prefers not to say" },
"2" => { "value" => "Part-time – Less than 30 hours" },
"3" => { "value" => "In government training into work, such as New Deal" },
"4" => { "value" => "Jobseeker" },
"5" => { "value" => "Retired" },
"6" => { "value" => "Not seeking work" },
"7" => { "value" => "Full-time student" },
"8" => { "value" => "Unable to work because of long term sick or disability" },
"divider" => { "value" => true },
}
end
it "does not include those options in the displayed options" do

2
spec/models/form/sales/pages/buyer1_previous_tenure_spec.rb

@ -20,6 +20,6 @@ RSpec.describe Form::Sales::Pages::Buyer1PreviousTenure, type: :model do
end
it "has the correct header" do
expect(page.header).to eq("What was buyer 1's previous tenure?")
expect(page.header).to be_nil
end
end

35
spec/models/form/sales/pages/previous_property_type_spec.rb

@ -0,0 +1,35 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::PreviousPropertyType, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[fromprop])
end
it "has the correct id" do
expect(page.id).to eq("previous_property_type")
end
it "has the correct header" do
expect(page.header).to eq("")
end
it "has the correct description" do
expect(page.description).to eq("")
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{
"soctenant" => 1,
}])
end
end

35
spec/models/form/sales/pages/previous_tenure_spec.rb

@ -0,0 +1,35 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::PreviousTenure, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[socprevten])
end
it "has the correct id" do
expect(page.id).to eq("shared_ownership_previous_tenure")
end
it "has the correct header" do
expect(page.header).to eq("")
end
it "has the correct description" do
expect(page.description).to eq("")
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{
"soctenant" => 1,
}])
end
end

47
spec/models/form/sales/questions/fromprop_spec.rb

@ -0,0 +1,47 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::Fromprop, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("fromprop")
end
it "has the correct header" do
expect(question.header).to eq("What was the previous property type?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Previous property type")
end
it "has the correct type" do
expect(question.type).to eq("radio")
end
it "is not marked as derived" do
expect(question.derived?).to be false
end
it "has the correct hint" do
expect(question.hint_text).to eq("")
end
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"1" => { "value" => "Flat or maisonette" },
"2" => { "value" => "Bedsit" },
"3" => { "value" => "House" },
"4" => { "value" => "Bungalow" },
"9" => { "value" => "Other" },
})
end
end

2
spec/models/form/sales/questions/gender_identity1_spec.rb

@ -40,7 +40,7 @@ RSpec.describe Form::Sales::Questions::GenderIdentity1, type: :model do
"F" => { "value" => "Female" },
"M" => { "value" => "Male" },
"X" => { "value" => "Non-binary" },
"R" => { "value" => "Prefers not to say " },
"R" => { "value" => "Prefers not to say" },
})
end

47
spec/models/form/sales/questions/previous_tenure_spec.rb

@ -0,0 +1,47 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::PreviousTenure, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("socprevten")
end
it "has the correct header" do
expect(question.header).to eq("What was the previous tenure of the buyer?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Previous property tenure")
end
it "has the correct type" do
expect(question.type).to eq("radio")
end
it "is not marked as derived" do
expect(question.derived?).to be false
end
it "has the correct hint" do
expect(question.hint_text).to eq("")
end
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"1" => { "value" => "Social Rent" },
"2" => { "value" => "Affordable Rent" },
"3" => { "value" => "London Affordable Rent" },
"9" => { "value" => "Other" },
"10" => { "value" => "Don’t know" },
})
end
end

2
spec/models/form/sales/subsections/property_information_spec.rb

@ -15,8 +15,8 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
expect(property_information.pages.map(&:id)).to eq(
%w[
property_number_of_bedrooms
property_building_type
property_unit_type
property_building_type
property_local_authority
property_wheelchair_accessible
],

2
spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb

@ -23,6 +23,8 @@ RSpec.describe Form::Sales::Subsections::SharedOwnershipScheme, type: :model do
la_nominations
buyer_previous
previous_bedrooms
previous_property_type
shared_ownership_previous_tenure
about_price_shared_ownership
mortgage_used_shared_ownership
mortgage_amount_shared_ownership

4
spec/models/form_handler_spec.rb

@ -52,14 +52,14 @@ RSpec.describe FormHandler do
it "is able to load a current sales form" do
form = form_handler.get_form("current_sales")
expect(form).to be_a(Form)
expect(form.pages.count).to eq(135)
expect(form.pages.count).to eq(137)
expect(form.name).to eq("2022_2023_sales")
end
it "is able to load a previous sales form" do
form = form_handler.get_form("previous_sales")
expect(form).to be_a(Form)
expect(form.pages.count).to eq(135)
expect(form.pages.count).to eq(137)
expect(form.name).to eq("2021_2022_sales")
end
end

6
spec/models/forms/bulk_upload_lettings/upload_your_file_spec.rb

@ -51,5 +51,11 @@ RSpec.describe Forms::BulkUploadLettings::UploadYourFile do
expect(Storage::S3Service).to have_received(:new)
expect(mock_storage_service).to have_received(:write_file).with(bulk_upload.identifier, actual_file.read)
end
it "enqueues job to process bulk upload" do
expect {
form.save!
}.to have_enqueued_job(ProcessBulkUploadJob)
end
end
end

6
spec/models/forms/bulk_upload_sales/upload_your_file_spec.rb

@ -49,5 +49,11 @@ RSpec.describe Forms::BulkUploadSales::UploadYourFile do
expect(Storage::S3Service).to have_received(:new)
expect(mock_storage_service).to have_received(:write_file).with(bulk_upload.identifier, actual_file.read)
end
it "enqueues job to process bulk upload" do
expect {
form.save!
}.to have_enqueued_job(ProcessBulkUploadJob)
end
end
end

10
spec/models/lettings_log_spec.rb

@ -715,9 +715,8 @@ RSpec.describe LettingsLog do
context "when tenant is in receipt of housing benefit and universal credit" do
it "correctly derives and saves weekly total shortfall" do
lettings_log.update!(hbrentshortfall: 1, tshortfall: 130, period: 6, hb: 8)
record_from_db = ActiveRecord::Base.connection.execute("select wtshortfall from lettings_logs where id=#{lettings_log.id}").to_a[0]
lettings_log.reload
expect(lettings_log.wtshortfall).to eq(122.5)
expect(record_from_db["wtshortfall"]).to eq(122.5)
end
end
end
@ -1908,7 +1907,7 @@ RSpec.describe LettingsLog do
end
context "when a question that has already been answered, no longer has met dependencies" do
let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, cbl: 1, preg_occ: 2, wchair: 1) }
let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, cbl: 1, preg_occ: 2, wchair: 2) }
it "clears the answer" do
expect { lettings_log.update!(preg_occ: nil) }.to change(lettings_log, :cbl).from(1).to(nil)
@ -1926,20 +1925,19 @@ RSpec.describe LettingsLog do
let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, illness: 1, illness_type_1: 1) }
it "clears the answer" do
expect { lettings_log.update!(illness: 0) }.to change(lettings_log, :illness_type_1).from(1).to(nil)
expect { lettings_log.update!(illness: 2) }.to change(lettings_log, :illness_type_1).from(1).to(nil)
end
end
end
context "with two pages having the same question key, only one's dependency is met" do
let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, cbl: 0, preg_occ: 2, wchair: 1) }
let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, cbl: 0, preg_occ: 2, wchair: 2) }
it "does not clear the value for answers that apply to both pages" do
expect(lettings_log.cbl).to eq(0)
end
it "does clear the value for answers that do not apply for invalidated page" do
lettings_log.update!({ wchair: 1, sex2: "F", age2: 33 })
lettings_log.update!({ cbl: 1 })
lettings_log.update!({ preg_occ: 1 })

16
spec/models/validations/financial_validations_spec.rb

@ -927,9 +927,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 4334
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "4", max_chcharge: 4333))
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "every calendar month", max_chcharge: 4333))
expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "4", max_chcharge: 4333))
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "every calendar month", max_chcharge: 4333))
end
it "validates charge when period is every 2 weeks" do
@ -937,9 +937,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 2001
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "2", max_chcharge: 2000))
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "every 2 weeks", max_chcharge: 2000))
expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "2", max_chcharge: 2000))
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "every 2 weeks", max_chcharge: 2000))
end
it "validates charge when period is every 4 weeks" do
@ -1015,9 +1015,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 42
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "4", max_chcharge: 4333))
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "every calendar month", max_chcharge: 4333))
expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "4", max_chcharge: 4333))
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 43, period: "every calendar month", max_chcharge: 4333))
end
it "validates charge when period is every 2 weeks" do
@ -1025,9 +1025,9 @@ RSpec.describe Validations::FinancialValidations do
record.chcharge = 19
financial_validator.validate_care_home_charges(record)
expect(record.errors["chcharge"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "2", max_chcharge: 2000))
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "every 2 weeks", max_chcharge: 2000))
expect(record.errors["period"])
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "2", max_chcharge: 2000))
.to include(match I18n.t("validations.financial.carehome.out_of_range", min_chcharge: 20, period: "every 2 weeks", max_chcharge: 2000))
end
it "validates charge when period is every 4 weeks" do

48
spec/models/validations/shared_validations_spec.rb

@ -1,7 +1,7 @@
require "rails_helper"
RSpec.describe Validations::SharedValidations do
subject(:household_validator) { validator_class.new }
subject(:shared_validator) { validator_class.new }
let(:validator_class) { Class.new { include Validations::SharedValidations } }
let(:record) { FactoryBot.create(:lettings_log) }
@ -15,57 +15,87 @@ RSpec.describe Validations::SharedValidations do
context "when validating age" do
it "validates that person 1's age is a number" do
record.age1 = "random"
household_validator.validate_numeric_min_max(record)
shared_validator.validate_numeric_min_max(record)
expect(record.errors["age1"])
.to include(match I18n.t("validations.numeric.valid", field: "Lead tenant’s age", min: 16, max: 120))
end
it "validates that other household member ages are a number" do
record.age2 = "random"
household_validator.validate_numeric_min_max(record)
shared_validator.validate_numeric_min_max(record)
expect(record.errors["age2"])
.to include(match I18n.t("validations.numeric.valid", field: "Person 2’s age", min: 1, max: 120))
end
it "validates that person 1's age is greater than 16" do
record.age1 = 15
household_validator.validate_numeric_min_max(record)
shared_validator.validate_numeric_min_max(record)
expect(record.errors["age1"])
.to include(match I18n.t("validations.numeric.valid", field: "Lead tenant’s age", min: 16, max: 120))
end
it "validates that other household member ages are greater than 1" do
record.age2 = 0
household_validator.validate_numeric_min_max(record)
shared_validator.validate_numeric_min_max(record)
expect(record.errors["age2"])
.to include(match I18n.t("validations.numeric.valid", field: "Person 2’s age", min: 1, max: 120))
end
it "validates that person 1's age is less than 121" do
record.age1 = 121
household_validator.validate_numeric_min_max(record)
shared_validator.validate_numeric_min_max(record)
expect(record.errors["age1"])
.to include(match I18n.t("validations.numeric.valid", field: "Lead tenant’s age", min: 16, max: 120))
end
it "validates that other household member ages are greater than 121" do
record.age2 = 123
household_validator.validate_numeric_min_max(record)
shared_validator.validate_numeric_min_max(record)
expect(record.errors["age2"])
.to include(match I18n.t("validations.numeric.valid", field: "Person 2’s age", min: 1, max: 120))
end
it "validates that person 1's age is between 16 and 120" do
record.age1 = 63
household_validator.validate_numeric_min_max(record)
shared_validator.validate_numeric_min_max(record)
expect(record.errors["age1"]).to be_empty
end
it "validates that other household member ages are between 1 and 120" do
record.age6 = 45
household_validator.validate_numeric_min_max(record)
shared_validator.validate_numeric_min_max(record)
expect(record.errors["age6"]).to be_empty
end
end
end
describe "radio options validations" do
it "allows only possible values" do
record.needstype = 1
shared_validator.validate_valid_radio_option(record)
expect(record.errors["needstype"]).to be_empty
end
it "denies impossible values" do
record.needstype = 3
shared_validator.validate_valid_radio_option(record)
expect(record.errors["needstype"]).to be_present
expect(record.errors["needstype"]).to eql(["Enter a valid value for needs type"])
end
context "when feature is toggled off" do
before do
allow(FeatureToggle).to receive(:validate_valid_radio_options?).and_return(false)
end
it "allows any values" do
record.needstype = 3
shared_validator.validate_valid_radio_option(record)
expect(record.errors["needstype"]).to be_empty
end
end
end
end

57
spec/requests/bulk_upload_lettings_results_controller_spec.rb

@ -0,0 +1,57 @@
require "rails_helper"
RSpec.describe BulkUploadLettingsResultsController, type: :request do
let(:user) { create(:user) }
let(:bulk_upload) { create(:bulk_upload, :lettings, user:, bulk_upload_errors:) }
let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2) }
before do
sign_in user
end
describe "GET /lettings-logs/bulk-upload-results/:ID" do
it "renders correct year" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response).to be_successful
expect(response.body).to include("Bulk Upload for lettings (2022/23)")
end
it "renders correct number of errors" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response).to be_successful
expect(response.body).to include("We found 2 errors in your file")
end
it "renders filename of the upload" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response).to be_successful
expect(response.body).to include(bulk_upload.filename)
end
context "when there are errors for more than 1 row" do
let(:bulk_upload_errors) { [bulk_upload_error_1, bulk_upload_error_2] }
let(:bulk_upload_error_1) { create(:bulk_upload_error, row: 1) }
let(:bulk_upload_error_2) { create(:bulk_upload_error, row: 2) }
it "renders no. of tables equal to no. of rows with errors" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response.body).to include("<table").twice
end
end
context "when viewing sales log" do
let(:bulk_upload) { create(:bulk_upload, :sales, user:, bulk_upload_errors:) }
it "renders a 404" do
get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response).not_to be_successful
expect(response).to be_not_found
end
end
end
end

70
spec/requests/bulk_upload_sales_results_controller_spec.rb

@ -0,0 +1,70 @@
require "rails_helper"
RSpec.describe BulkUploadSalesResultsController, type: :request do
let(:user) { create(:user) }
let(:bulk_upload) { create(:bulk_upload, :sales, user:, bulk_upload_errors:) }
let(:bulk_upload_errors) { create_list(:bulk_upload_error, 2) }
before do
sign_in user
end
describe "GET /sales-logs/bulk-upload-results/:ID" do
it "renders correct year" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response).to be_successful
expect(response.body).to include("Bulk Upload for sales (2022/23)")
end
it "renders correct number of errors" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response).to be_successful
expect(response.body).to include("We found 2 errors in your file")
end
it "renders filename of the upload" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response).to be_successful
expect(response.body).to include(bulk_upload.filename)
end
it "renders Purchaser code" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response.body).to include("Purchaser code: #{bulk_upload.bulk_upload_errors.first.purchaser_code}")
end
it "does not render tenant code or property reference" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response.body).not_to include("Tenant code:")
expect(response.body).not_to include("Property reference:")
end
context "when there are errors for more than 1 row" do
let(:bulk_upload_errors) { [bulk_upload_error_1, bulk_upload_error_2] }
let(:bulk_upload_error_1) { create(:bulk_upload_error, row: 1) }
let(:bulk_upload_error_2) { create(:bulk_upload_error, row: 2) }
it "renders no. of tables equal to no. of rows with errors" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response.body).to include("<table").twice
end
end
context "when viewing lettings log" do
let(:bulk_upload) { create(:bulk_upload, :lettings, user:, bulk_upload_errors:) }
it "renders a 404" do
get "/sales-logs/bulk-upload-results/#{bulk_upload.id}"
expect(response).not_to be_successful
expect(response).to be_not_found
end
end
end
end

23
spec/services/bulk_upload/downloader_spec.rb

@ -12,7 +12,6 @@ RSpec.describe BulkUpload::Downloader do
io
end
# rubocop:disable RSpec/PredicateMatcher
describe "#call" do
let(:mock_storage_service) { instance_double(Storage::S3Service, get_file_io:) }
@ -23,9 +22,27 @@ RSpec.describe BulkUpload::Downloader do
expect(mock_storage_service).to have_received(:get_file_io).with(bulk_upload.identifier)
expect(File.exist?(downloader.path)).to be_truthy
expect(File).to exist(downloader.path)
expect(File.read(downloader.path)).to eql("hello")
end
end
# rubocop:enable RSpec/PredicateMatcher
describe "#delete_local_file!" do
let(:mock_storage_service) { instance_double(Storage::S3Service, get_file_io:) }
it "deletes the local file" do
allow(Storage::S3Service).to receive(:new).and_return(mock_storage_service)
downloader.call
expect(File).to exist(downloader.path)
expect(File.read(downloader.path)).to eql("hello")
path = downloader.path
downloader.delete_local_file!
expect(File).not_to exist(path)
end
end
end

81
spec/services/bulk_upload/lettings/row_parser_spec.rb

@ -0,0 +1,81 @@
require "rails_helper"
RSpec.describe BulkUpload::Lettings::RowParser do
subject(:parser) { described_class.new(attributes) }
describe "validations" do
before do
parser.valid?
end
describe "field_1" do
context "when null" do
let(:attributes) { { field_1: nil } }
it "returns an error" do
expect(parser.errors).to include(:field_1)
end
end
context "when outside permited range" do
let(:attributes) { { field_1: "13" } }
it "returns an error" do
expect(parser.errors).to include(:field_1)
end
end
context "when valid" do
let(:attributes) { { field_1: 1 } }
it "is valid" do
expect(parser.errors).not_to include(:field_1)
end
end
end
describe "field_4" do
context "when text" do
let(:attributes) { { field_4: "R" } }
it "is not valid" do
expect(parser.errors).to include(:field_4)
end
end
context "when valid" do
let(:attributes) { { field_4: "3" } }
it "is valid" do
expect(parser.errors).not_to include(:field_4)
end
end
context "when allowed to be null" do
let(:attributes) { { field_1: "2", field_4: "" } }
it "is valid" do
expect(parser.errors).not_to include(:field_4)
end
end
context "when not allowed to be null" do
let(:attributes) { { field_1: "3", field_4: "" } }
it "is not valid" do
expect(parser.errors).to include(:field_4)
end
end
end
describe "#field_134" do
context "when not a possible value" do
let(:attributes) { { field_134: "3" } }
it "is not valid" do
expect(parser.errors).to include(:field_134)
end
end
end
end
end

39
spec/services/bulk_upload/lettings/validator_spec.rb

@ -0,0 +1,39 @@
require "rails_helper"
RSpec.describe BulkUpload::Lettings::Validator do
subject(:validator) { described_class.new(bulk_upload:, path:) }
let(:bulk_upload) { create(:bulk_upload) }
let(:path) { file.path }
let(:file) { Tempfile.new }
describe "validations" do
context "when file is empty" do
it "is not valid" do
expect(validator).not_to be_valid
end
end
context "when file has too many columns" do
before do
file.write("a," * 136)
file.write("\n")
file.rewind
end
it "is not valid" do
expect(validator).not_to be_valid
end
end
context "when incorrect headers"
end
context "when a valid csv" do
let(:path) { file_fixture("2021_22_lettings_bulk_upload.csv") }
it do
validator.call
end
end
end

34
spec/services/bulk_upload/processor_spec.rb

@ -0,0 +1,34 @@
require "rails_helper"
RSpec.describe BulkUpload::Processor do
subject(:processor) { described_class.new(bulk_upload:) }
let(:bulk_upload) { create(:bulk_upload, :lettings) }
context "when processing a bulk upload with errors" do
describe "#call" do
let(:mock_downloader) do
instance_double(
BulkUpload::Downloader,
call: nil,
path: file_fixture("2021_22_lettings_bulk_upload.csv"),
delete_local_file!: nil,
)
end
it "persist the validation errors" do
allow(BulkUpload::Downloader).to receive(:new).with(bulk_upload:).and_return(mock_downloader)
expect { processor.call }.to change(BulkUploadError, :count).by(9)
end
it "deletes the local file afterwards" do
allow(BulkUpload::Downloader).to receive(:new).with(bulk_upload:).and_return(mock_downloader)
processor.call
expect(mock_downloader).to have_received(:delete_local_file!)
end
end
end
end

21
spec/services/bulk_upload/sales/row_parser_spec.rb

@ -0,0 +1,21 @@
require "rails_helper"
RSpec.describe BulkUpload::Sales::RowParser do
subject(:parser) { described_class.new(attributes) }
describe "validations" do
before do
parser.valid?
end
describe "#field_117" do
context "when not a possible value" do
let(:attributes) { { field_117: "3" } }
it "is not valid" do
expect(parser.errors).to include(:field_117)
end
end
end
end
end

47
spec/services/bulk_upload/sales/validator_spec.rb

@ -0,0 +1,47 @@
require "rails_helper"
RSpec.describe BulkUpload::Sales::Validator do
subject(:validator) { described_class.new(bulk_upload:, path:) }
let(:bulk_upload) { create(:bulk_upload) }
let(:path) { file.path }
let(:file) { Tempfile.new }
describe "validations" do
context "when file is empty" do
it "is not valid" do
expect(validator).not_to be_valid
end
end
context "when file has too many columns" do
before do
file.write((%w[a] * 127).join(","))
file.rewind
end
it "is not valid" do
expect(validator).not_to be_valid
end
end
context "when incorrect headers"
end
context "when a valid csv that contains errors" do
let(:path) { file_fixture("2022_23_sales_bulk_upload.csv") }
it "persists bulk upload errors" do
expect {
validator.call
}.to change(BulkUploadError, :count).by(1)
end
it "populates purchaser_code" do
validator.call
error = BulkUploadError.last
expect(error.purchaser_code).to eql("1")
end
end
end

1
spec/services/csv/lettings_log_csv_service_spec.rb

@ -196,7 +196,6 @@ RSpec.describe Csv::LettingsLogCsvService do
irproduct
old_id
joint
illness_type_0
hhtype
new_old
vacdays

6
spec/services/exports/lettings_log_export_service_spec.rb

@ -59,7 +59,7 @@ RSpec.describe Exports::LettingsLogExportService do
end
context "and one lettings log is available for export" do
let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.utc(2022, 2, 2, 10, 36, 49), tenancylength: 5) }
let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.utc(2022, 2, 2, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4) }
it "generates a ZIP export file with the expected filename" do
expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args)
@ -237,7 +237,7 @@ RSpec.describe Exports::LettingsLogExportService do
let(:csv_export_file) { File.open("spec/fixtures/exports/general_needs_log.csv", "r:UTF-8") }
let(:expected_csv_filename) { "export_2022_05_01.csv" }
let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.utc(2022, 2, 2, 10, 36, 49), tenancylength: 5) }
let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, propcode: "123", ppostcode_full: "SE2 6RT", postcode_full: "NW1 5TY", tenancycode: "BZ737", startdate: Time.utc(2022, 2, 2, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4) }
it "generates an CSV export file with the expected content" do
expected_content = replace_entity_ids(lettings_log, csv_export_file.read)
@ -256,7 +256,7 @@ RSpec.describe Exports::LettingsLogExportService do
let(:scheme) { FactoryBot.create(:scheme, :export, owning_organisation: organisation) }
let(:location) { FactoryBot.create(:location, :export, scheme:, startdate: Time.zone.local(2021, 4, 1)) }
let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, :export, :sh, scheme:, location:, created_by: user, owning_organisation: organisation, startdate: Time.utc(2022, 2, 2, 10, 36, 49)) }
let(:lettings_log) { FactoryBot.create(:lettings_log, :completed, :export, :sh, scheme:, location:, created_by: user, owning_organisation: organisation, startdate: Time.utc(2022, 2, 2, 10, 36, 49), underoccupation_benefitcap: 4, sheltered: 1) }
it "generates an XML export file with the expected content" do
expected_content = replace_entity_ids(lettings_log, export_file.read)

Loading…
Cancel
Save