Browse Source

Merge branch 'main' into CLDC-1917-allow-23-24-form

CLDC-1917-allow-23-24-form
natdeanlewissoftwire 2 years ago
parent
commit
7cb7263055
  1. 1
      Gemfile
  2. 3
      Gemfile.lock
  3. 13
      app/components/bulk_upload_error_summary_table_component.rb
  4. 19
      app/components/check_answers_summary_list_card_component.rb
  5. 2
      app/components/search_component.rb
  6. 5
      app/controllers/bulk_upload_lettings_logs_controller.rb
  7. 5
      app/controllers/bulk_upload_sales_logs_controller.rb
  8. 2
      app/controllers/form_controller.rb
  9. 5
      app/helpers/collection_time_helper.rb
  10. 16
      app/helpers/logs_helper.rb
  11. 47
      app/mailers/bulk_upload_mailer.rb
  12. 30
      app/models/derived_variables/lettings_log_variables.rb
  13. 9
      app/models/derived_variables/sales_log_variables.rb
  14. 14
      app/models/form/lettings/pages/person_age.rb
  15. 4
      app/models/form/lettings/pages/previous_housing_situation.rb
  16. 4
      app/models/form/lettings/pages/previous_housing_situation_renewal.rb
  17. 2
      app/models/form/lettings/pages/property_let_type.rb
  18. 4
      app/models/form/lettings/pages/property_wheelchair_accessible.rb
  19. 6
      app/models/form/lettings/pages/sheltered_accommodation.rb
  20. 5
      app/models/form/lettings/questions/age.rb
  21. 2
      app/models/form/lettings/questions/housingneeds_other.rb
  22. 2
      app/models/form/lettings/questions/net_income_known.rb
  23. 4
      app/models/form/lettings/questions/offered_social_let.rb
  24. 2
      app/models/form/lettings/questions/person_relationship.rb
  25. 7
      app/models/form/lettings/questions/previous_let_type.rb
  26. 41
      app/models/form/lettings/questions/previous_tenure.rb
  27. 3
      app/models/form/lettings/questions/previous_tenure_renewal.rb
  28. 87
      app/models/form/lettings/questions/prevten.rb
  29. 3
      app/models/form/lettings/questions/rsnvac.rb
  30. 1
      app/models/form/lettings/questions/sheltered.rb
  31. 1
      app/models/form/lettings/questions/voiddate.rb
  32. 7
      app/models/form/lettings/questions/wheelchair.rb
  33. 7
      app/models/form/lettings/subsections/household_characteristics.rb
  34. 2
      app/models/form/lettings/subsections/tenancy_information.rb
  35. 9
      app/models/form/sales/pages/about_staircase.rb
  36. 9
      app/models/form/sales/pages/buyer_previous.rb
  37. 9
      app/models/form/sales/pages/housing_benefits.rb
  38. 10
      app/models/form/sales/pages/number_of_others_in_property.rb
  39. 9
      app/models/form/sales/pages/previous_ownership.rb
  40. 3
      app/models/form/sales/pages/privacy_notice.rb
  41. 12
      app/models/form/sales/questions/buyer1_mortgage.rb
  42. 29
      app/models/form/sales/questions/buyer1_previous_tenure.rb
  43. 12
      app/models/form/sales/questions/buyer2_mortgage.rb
  44. 8
      app/models/form/sales/questions/buyer_previous.rb
  45. 10
      app/models/form/sales/questions/buyers_organisations.rb
  46. 6
      app/models/form/sales/questions/housing_benefits.rb
  47. 8
      app/models/form/sales/questions/mortgageused.rb
  48. 18
      app/models/form/sales/questions/number_of_others_in_property.rb
  49. 8
      app/models/form/sales/questions/prevown.rb
  50. 2
      app/models/form/sales/questions/property_local_authority_known.rb
  51. 35
      app/models/form/sales/questions/shared_ownership_type.rb
  52. 8
      app/models/form/sales/questions/staircase_owned.rb
  53. 17
      app/models/form/sales/subsections/household_characteristics.rb
  54. 6
      app/models/form/sales/subsections/income_benefits_and_savings.rb
  55. 10
      app/models/form/sales/subsections/outright_sale.rb
  56. 6
      app/models/form/sales/subsections/shared_ownership_scheme.rb
  57. 4
      app/models/forms/bulk_upload_lettings/prepare_your_file.rb
  58. 4
      app/models/forms/bulk_upload_sales/prepare_your_file.rb
  59. 7
      app/models/lettings_log.rb
  60. 2
      app/models/local_authority.rb
  61. 12
      app/models/log.rb
  62. 27
      app/models/sales_log.rb
  63. 10
      app/models/validations/financial_validations.rb
  64. 2
      app/models/validations/household_validations.rb
  65. 4
      app/models/validations/sales/household_validations.rb
  66. 2
      app/models/validations/sales/sale_information_validations.rb
  67. 2
      app/models/validations/sales/soft_validations.rb
  68. 1
      app/services/bulk_upload/lettings/csv_parser.rb
  69. 130
      app/services/bulk_upload/lettings/row_parser.rb
  70. 15
      app/services/bulk_upload/lettings/validator.rb
  71. 36
      app/services/bulk_upload/processor.rb
  72. 145
      app/services/imports/lettings_logs_import_service.rb
  73. 25
      app/services/imports/local_authorities_service.rb
  74. 127
      app/services/imports/logs_import_service.rb
  75. 549
      app/services/imports/sales_logs_import_service.rb
  76. 2
      app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb
  77. 8
      app/views/bulk_upload_lettings_results/show.html.erb
  78. 2
      app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb
  79. 52
      app/views/bulk_upload_shared/guidance.html.erb
  80. 2
      app/views/form/_date_question.html.erb
  81. 1
      app/views/form/headers/_person_6_known_page.erb
  82. 4
      app/views/logs/index.html.erb
  83. 1
      config/initializers/date_formats.rb
  84. 2
      config/initializers/feature_toggle.rb
  85. 388
      config/local_authorities_data/initial_local_authorities.csv
  86. 20
      config/locales/en.yml
  87. 2
      config/routes.rb
  88. 32
      config/sale_range_data/2023.csv
  89. 8
      db/migrate/20230215112932_add_old_id_to_sales_logs.rb
  90. 5
      db/migrate/20230301120116_add_category_to_bulk_upload_errors.rb
  91. 5
      db/migrate/20230301144555_add_pregblank.rb
  92. 13
      db/migrate/20230308101826_create_local_authorities.rb
  93. 24
      db/schema.rb
  94. 7
      db/seeds.rb
  95. 2
      lib/tasks/data_import.rake
  96. 1
      lib/tasks/full_import.rake
  97. 13
      lib/tasks/local_authorities.rake
  98. BIN
      public/files/bulk-upload-lettings-specification-2022-23.xlsx
  99. BIN
      public/files/bulk-upload-sales-specification-2022-23.xlsx
  100. 35
      spec/components/bulk_upload_error_summary_table_component_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

1
Gemfile

@ -49,6 +49,7 @@ gem "paper_trail"
# Store active record objects in version whodunnits
gem "paper_trail-globalid"
# Request rate limiting
gem "rack", ">= 2.2.6.3"
gem "rack-attack"
gem "redis", "~> 4.8"
# Receive exceptions and configure alerts

3
Gemfile.lock

@ -279,7 +279,7 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.6.2)
rack (2.2.6.2)
rack (2.2.6.3)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-mini-profiler (2.3.4)
@ -478,6 +478,7 @@ DEPENDENCIES
propshaft
pry-byebug
puma (~> 5.0)
rack (>= 2.2.6.3)
rack-attack
rack-mini-profiler (~> 2.0)
rails (~> 7.0.2)

13
app/components/bulk_upload_error_summary_table_component.rb

@ -1,4 +1,6 @@
class BulkUploadErrorSummaryTableComponent < ViewComponent::Base
DISPLAY_THRESHOLD = 16
attr_reader :bulk_upload
def initialize(bulk_upload:)
@ -11,7 +13,18 @@ class BulkUploadErrorSummaryTableComponent < ViewComponent::Base
@sorted_errors ||= bulk_upload
.bulk_upload_errors
.group(:col, :field, :error)
.having("count(*) > ?", display_threshold)
.count
.sort_by { |el| el[0][0].rjust(3, "0") }
end
def errors?
sorted_errors.present?
end
private
def display_threshold
DISPLAY_THRESHOLD
end
end

19
app/components/check_answers_summary_list_card_component.rb

@ -18,21 +18,10 @@ class CheckAnswersSummaryListCardComponent < ViewComponent::Base
end
def check_answers_card_title(question)
if question.form.type == "lettings"
case question.check_answers_card_number
when 1
"Lead tenant"
when 2..8
"Person #{question.check_answers_card_number}"
end
else
case question.check_answers_card_number
when 1..number_of_buyers
"Buyer #{question.check_answers_card_number}"
when (number_of_buyers + 1)..(number_of_buyers + 4)
"Person #{question.check_answers_card_number}"
end
end
return "Lead tenant" if question.form.type == "lettings" && question.check_answers_card_number == 1
return "Buyer #{question.check_answers_card_number}" if question.check_answers_card_number <= number_of_buyers
"Person #{question.check_answers_card_number}"
end
private

2
app/components/search_component.rb

@ -23,6 +23,8 @@ class SearchComponent < ViewComponent::Base
user_path(current_user)
elsif request.path.include?("organisations")
organisations_path
elsif request.path.include?("sales-logs")
sales_logs_path
elsif request.path.include?("logs")
lettings_logs_path
end

5
app/controllers/bulk_upload_lettings_logs_controller.rb

@ -21,6 +21,11 @@ class BulkUploadLettingsLogsController < ApplicationController
end
end
def guidance
@form = Forms::BulkUploadLettings::PrepareYourFile.new
render "bulk_upload_shared/guidance"
end
private
def current_year

5
app/controllers/bulk_upload_sales_logs_controller.rb

@ -21,6 +21,11 @@ class BulkUploadSalesLogsController < ApplicationController
end
end
def guidance
@form = Forms::BulkUploadSales::PrepareYourFile.new
render "bulk_upload_shared/guidance"
end
private
def current_year

2
app/controllers/form_controller.rb

@ -54,7 +54,7 @@ class FormController < ApplicationController
if @page.routed_to?(@log, current_user)
render "form/page"
else
redirect_to lettings_log_path(@log)
redirect_to @log.lettings? ? lettings_log_path(@log) : sales_log_path(@log)
end
else
render_not_found

5
app/helpers/collection_time_helper.rb

@ -12,6 +12,11 @@ module CollectionTimeHelper
Time.zone.local(collection_start_year(date), 4, 1)
end
def date_mid_collection_year_formatted(date)
example_date = date.nil? ? Time.zone.today : collection_start_date(date).to_date + 5.months
example_date.to_formatted_s(:govuk_date_number_month)
end
def current_collection_start_date
Time.zone.local(current_collection_start_year, 4, 1)
end

16
app/helpers/logs_helper.rb

@ -23,4 +23,20 @@ module LogsHelper
array = bulk_upload ? [bulk_upload.id] : []
array.index_with { |_bulk_upload_id| "With logs from bulk upload" }
end
def search_label_for_controller(controller)
case log_type_for_controller(controller)
when "lettings"
"Search by log ID, tenant code, property reference or postcode"
when "sales"
"Search by log ID, purchaser code or postcode"
end
end
def csv_download_url_for_controller(controller)
case log_type_for_controller(controller)
when "lettings"
csv_download_lettings_logs_path(search: params["search"])
end
end
end

47
app/mailers/bulk_upload_mailer.rb

@ -46,6 +46,12 @@ class BulkUploadMailer < NotifyMailer
def send_correct_and_upload_again_mail(bulk_upload:)
error_description = "We noticed that you have a lot of similar errors in column #{columns_with_errors(bulk_upload:)}. Please correct your data export and upload again."
summary_report_link = if BulkUploadErrorSummaryTableComponent.new(bulk_upload:).errors?
summary_bulk_upload_lettings_result_url(bulk_upload)
else
bulk_upload_lettings_result_url(bulk_upload)
end
send_email(
bulk_upload.user.email,
BULK_UPLOAD_FAILED_CSV_ERRORS_TEMPLATE_ID,
@ -55,22 +61,45 @@ class BulkUploadMailer < NotifyMailer
year_combo: bulk_upload.year_combo,
lettings_or_sales: bulk_upload.log_type,
error_description:,
summary_report_link: summary_bulk_upload_lettings_result_url(bulk_upload),
summary_report_link:,
},
)
end
def send_bulk_upload_failed_file_setup_error_mail(user, bulk_upload)
def send_bulk_upload_failed_file_setup_error_mail(bulk_upload:)
bulk_upload_link = if bulk_upload.lettings?
start_bulk_upload_lettings_logs_url
else
start_bulk_upload_sales_logs_url
end
validator_class = if bulk_upload.lettings?
BulkUpload::Lettings::Validator
else
BulkUpload::Sales::Validator
end
errors = bulk_upload
.bulk_upload_errors
.where(category: "setup")
.group(:col, :field)
.count
.keys
.sort_by { |_col, field| field }
.map do |col, field|
"- Column #{col} (#{validator_class.question_for_field(field.to_sym)})"
end
send_email(
user.email,
bulk_upload.user.email,
BULK_UPLOAD_FAILED_FILE_SETUP_ERROR_TEMPLATE_ID,
{
filename: "[#{bulk_upload} filename]",
upload_timestamp: "[#{bulk_upload} upload_timestamp]",
lettings_or_sales: "[#{bulk_upload} lettings_or_sales]",
year_combo: "[#{bulk_upload} year_combo]",
errors_list: "[#{bulk_upload} errors_list]",
bulk_upload_link: "[#{bulk_upload} bulk_upload_link]",
filename: bulk_upload.filename,
upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time),
lettings_or_sales: bulk_upload.log_type,
year_combo: bulk_upload.year_combo,
errors_list: errors.join("\n"),
bulk_upload_link:,
},
)
end

30
app/models/derived_variables/lettings_log_variables.rb

@ -1,5 +1,31 @@
module DerivedVariables::LettingsLogVariables
RENT_TYPE_MAPPING = { 0 => 1, 1 => 2, 2 => 2, 3 => 3, 4 => 3, 5 => 3 }.freeze
# renttype and unitletas values are different for intermediate rent (3 for renttype and 4 for unitletas)
RENT_TYPE_MAPPING = {
0 => 1, # "Social Rent" => "Social Rent"
1 => 2, # "Affordable Rent" => "Affordable Rent"
2 => 2, # "London Affordable Rent" => "Affordable Rent"
3 => 3, # "Rent to Buy" => "Intermediate Rent"
4 => 3, # "London Living Rent" => "Intermediate Rent"
5 => 3, # "Other intermediate rent product" => "Intermediate Rent"
}.freeze
UNITLETAS_MAPPING = {
0 => 1, # "Social Rent" => "Social Rent basis"
1 => 2, # "Affordable Rent" => "Affordable Rent basis"
2 => 2, # "London Affordable Rent" => "Affordable Rent basis"
3 => 4, # "Rent to Buy" => "Intermediate Rent basis"
4 => 4, # "London Living Rent" => "Intermediate Rent basis"
5 => 4, # "Other intermediate rent product" => "Intermediate Rent basis"
}.freeze
UNITLETAS_MAPPING_23_24 = {
0 => 1, # "Social Rent" => "Social Rent basis"
1 => 2, # "Affordable Rent" => "Affordable Rent basis"
2 => 5, # "London Affordable Rent" => "London Affordable Rent basis"
3 => 6, # "Rent to Buy" => "Rent to Buy basis"
4 => 7, # "London Living Rent" => "London Living Rent basis"
5 => 8, # "Other intermediate rent product" => "Another Intermediate Rent basis"
}.freeze
def scheme_has_multiple_locations?
return false unless scheme
@ -48,6 +74,8 @@ module DerivedVariables::LettingsLogVariables
self.offered = 0
self.voiddate = startdate
self.first_time_property_let_as_social_housing = 0
self.rsnvac = 14
self.unitletas = form.start_date.year >= 2023 ? UNITLETAS_MAPPING_23_24[rent_type] : UNITLETAS_MAPPING[rent_type]
if is_general_needs?
# fixed term
self.prevten = 32 if managing_organisation&.provider_type == "PRP"

9
app/models/derived_variables/sales_log_variables.rb

@ -21,12 +21,19 @@ module DerivedVariables::SalesLogVariables
self.pcode1, self.pcode2 = postcode_full.split(" ") if postcode_full.present?
self.totchild = total_child
self.totadult = total_adult + total_elder
self.hhmemb = totchild + totadult
self.hhmemb = number_of_household_members
self.hhtype = household_type
end
private
def number_of_household_members
return unless hholdcount.present? && jointpur.present?
number_of_buyers = joint_purchase? ? 2 : 1
hholdcount + number_of_buyers
end
def total_elder
ages = [age1, age2, age3, age4, age5, age6]
ages.count { |age| age.present? && age >= 60 }

14
app/models/form/lettings/pages/person_age.rb

@ -1,15 +1,21 @@
class Form::Lettings::Pages::PersonAge < ::Form::Page
def initialize(id, hsh, subsection, person_index:)
def initialize(id, hsh, subsection, person_index:, person_type: "non_child")
super(id, hsh, subsection)
@id = "person_#{person_index}_age"
@depends_on = [{ "details_known_#{person_index}" => 0 }]
@id = "person_#{person_index}_age_#{person_type}"
@person_index = person_index
@person_type = person_type
@depends_on = [
{
"details_known_#{person_index}" => 0,
"person_#{person_index}_child_relation?" => (person_type == "child"),
},
]
end
def questions
@questions ||= [
Form::Lettings::Questions::AgeKnown.new(nil, nil, self, person_index: @person_index),
Form::Lettings::Questions::Age.new(nil, nil, self, person_index: @person_index),
Form::Lettings::Questions::Age.new(nil, nil, self, person_index: @person_index, person_type: @person_type),
]
end
end

4
app/models/form/lettings/pages/previous_housing_situation.rb

@ -2,10 +2,10 @@ class Form::Lettings::Pages::PreviousHousingSituation < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "previous_housing_situation"
@depends_on = [{ "renewal" => 0 }]
@depends_on = [{ "is_renewal?" => false }]
end
def questions
@questions ||= [Form::Lettings::Questions::Prevten.new(nil, nil, self)]
@questions ||= [Form::Lettings::Questions::PreviousTenure.new(nil, nil, self)]
end
end

4
app/models/form/lettings/pages/previous_housing_situation_renewal.rb

@ -2,10 +2,10 @@ class Form::Lettings::Pages::PreviousHousingSituationRenewal < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "previous_housing_situation_renewal"
@depends_on = [{ "renewal" => 1, "needstype" => 2 }]
@depends_on = [{ "is_renewal?" => true, "is_supported_housing?" => true }]
end
def questions
@questions ||= [Form::Lettings::Questions::PrevtenRenewal.new(nil, nil, self)]
@questions ||= [Form::Lettings::Questions::PreviousTenureRenewal.new(nil, nil, self)]
end
end

2
app/models/form/lettings/pages/property_let_type.rb

@ -6,6 +6,6 @@ class Form::Lettings::Pages::PropertyLetType < ::Form::Page
end
def questions
@questions ||= [Form::Lettings::Questions::Unitletas.new(nil, nil, self)]
@questions ||= [Form::Lettings::Questions::PreviousLetType.new(nil, nil, self)]
end
end

4
app/models/form/lettings/pages/property_wheelchair_accessible.rb

@ -2,10 +2,10 @@ class Form::Lettings::Pages::PropertyWheelchairAccessible < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "property_wheelchair_accessible"
@depends_on = [{ "needstype" => 1 }]
@depends_on = [{ "is_supported_housing?" => false }]
end
def questions
@questions ||= [Form::Lettings::Questions::Wchair.new(nil, nil, self)]
@questions ||= [Form::Lettings::Questions::Wheelchair.new(nil, nil, self)]
end
end

6
app/models/form/lettings/pages/shelteredaccom.rb → app/models/form/lettings/pages/sheltered_accommodation.rb

@ -1,8 +1,8 @@
class Form::Lettings::Pages::Shelteredaccom < ::Form::Page
class Form::Lettings::Pages::ShelteredAccommodation < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "shelteredaccom"
@depends_on = [{ "needstype" => 2 }]
@id = "sheltered_accommodation"
@depends_on = [{ "is_supported_housing?" => true }]
end
def questions

5
app/models/form/lettings/questions/age.rb

@ -1,5 +1,5 @@
class Form::Lettings::Questions::Age < ::Form::Question
def initialize(id, hsh, page, person_index:)
def initialize(id, hsh, page, person_index:, person_type:)
super(id, hsh, page)
@id = "age#{person_index}"
@check_answer_label = "Person #{person_index}’s age"
@ -9,7 +9,8 @@ class Form::Lettings::Questions::Age < ::Form::Question
@inferred_check_answers_value = [{ "condition" => { "age#{person_index}_known" => 1 }, "value" => "Not known" }]
@check_answers_card_number = person_index
@max = 120
@min = 0
@min = 1
@step = 1
@hint_text = "For a child under 1, enter 1" if person_type == "child"
end
end

2
app/models/form/lettings/questions/housingneeds_other.rb

@ -3,7 +3,7 @@ class Form::Lettings::Questions::HousingneedsOther < ::Form::Question
super
@id = "housingneeds_other"
@check_answer_label = "Other disabled access needs"
@header = "Do they have any other access needs?"
@header = "Do they have any other disabled access needs?"
@type = "radio"
@check_answers_card_number = 0
@hint_text = ""

2
app/models/form/lettings/questions/net_income_known.rb

@ -2,7 +2,7 @@ class Form::Lettings::Questions::NetIncomeKnown < ::Form::Question
def initialize(id, hsh, page)
super
@id = "net_income_known"
@check_answer_label = "Do you know the household’s combined income?"
@check_answer_label = "Do you know the household’s combined total income after tax?"
@header = "Do you know the household’s combined income after tax?"
@type = "radio"
@check_answers_card_number = 0

4
app/models/form/lettings/questions/offered_social_let.rb

@ -3,13 +3,13 @@ class Form::Lettings::Questions::OfferedSocialLet < ::Form::Question
super
@id = "offered"
@check_answer_label = "Times previously offered since becoming available"
@header = "Since becoming available, how many times has the property been previously offered?"
@header = "How many times was the property offered between becoming vacant and this letting?"
@type = "numeric"
@width = 2
@check_answers_card_number = 0
@max = 150
@min = 0
@hint_text = "If the property is being offered for let for the first time, enter 0."
@hint_text = "Do not include the offer that led to this letting.This is after the last tenancy ended. If the property is being offered for let for the first time, enter 0."
@step = 1
end
end

2
app/models/form/lettings/questions/person_relationship.rb

@ -14,7 +14,7 @@ class Form::Lettings::Questions::PersonRelationship < ::Form::Question
"P" => { "value" => "Partner" },
"C" => {
"value" => "Child",
"hint" => "Must be eligible for child benefit, aged under 16 or under 20 if still in full-time education.",
"hint" => "Must be eligible for child benefit: under age 16 or under 20 if still in full-time education.",
},
"X" => { "value" => "Other" },
"divider" => { "value" => true },

7
app/models/form/lettings/questions/unitletas.rb → app/models/form/lettings/questions/previous_let_type.rb

@ -1,4 +1,4 @@
class Form::Lettings::Questions::Unitletas < ::Form::Question
class Form::Lettings::Questions::PreviousLetType < ::Form::Question
def initialize(id, hsh, page)
super
@id = "unitletas"
@ -13,7 +13,10 @@ class Form::Lettings::Questions::Unitletas < ::Form::Question
ANSWER_OPTIONS = {
"1" => { "value" => "Social rent basis" },
"2" => { "value" => "Affordable rent basis" },
"4" => { "value" => "Intermediate rent basis" },
"5" => { "value" => "A London Affordable Rent basis" },
"6" => { "value" => "A Rent to Buy basis" },
"7" => { "value" => "A London Living Rent basis" },
"8" => { "value" => "Another Intermediate Rent basis" },
"divider" => { "value" => true },
"3" => { "value" => "Don’t know" },
}.freeze

41
app/models/form/lettings/questions/previous_tenure.rb

@ -0,0 +1,41 @@
class Form::Lettings::Questions::PreviousTenure < ::Form::Question
def initialize(id, hsh, page)
super
@id = "prevten"
@check_answer_label = "Where was the household immediately before this letting?"
@header = "Where was the household immediately before this letting?"
@type = "radio"
@check_answers_card_number = 0
@hint_text = "This is where the household was the night before they moved."
@answer_options = ANSWER_OPTIONS
end
ANSWER_OPTIONS = {
"30" => { "value" => "Fixed-term local authority general needs tenancy" },
"32" => { "value" => "Fixed-term private registered provider (PRP) general needs tenancy" },
"31" => { "value" => "Lifetime local authority general needs tenancy" },
"33" => { "value" => "Lifetime private registered provider (PRP) general needs tenancy" },
"34" => { "value" => "Specialist retirement housing" },
"36" => { "value" => "Sheltered housing for adults aged under 55 years" },
"35" => { "value" => "Extra care housing" },
"6" => { "value" => "Other supported housing" },
"3" => { "value" => "Private sector tenancy" },
"27" => { "value" => "Owner occupation (low-cost home ownership)" },
"26" => { "value" => "Owner occupation (private)" },
"28" => { "value" => "Living with friends or family" },
"14" => { "value" => "Bed and breakfast" },
"7" => { "value" => "Direct access hostel" },
"10" => { "value" => "Hospital" },
"29" => { "value" => "Prison or approved probation hostel" },
"19" => { "value" => "Rough sleeping" },
"18" => { "value" => "Any other temporary accommodation" },
"13" => { "value" => "Children’s home or foster care" },
"24" => { "value" => "Home Office Asylum Support" },
"37" => { "value" => "Host family or similar refugee accommodation" },
"23" => { "value" => "Mobile home or caravan" },
"21" => { "value" => "Refuge" },
"9" => { "value" => "Residential care home" },
"4" => { "value" => "Tied housing or rented with job" },
"25" => { "value" => "Any other accommodation" },
}.freeze
end

3
app/models/form/lettings/questions/prevten_renewal.rb → app/models/form/lettings/questions/previous_tenure_renewal.rb

@ -1,4 +1,4 @@
class Form::Lettings::Questions::PrevtenRenewal < ::Form::Question
class Form::Lettings::Questions::PreviousTenureRenewal < ::Form::Question
def initialize(id, hsh, page)
super
@id = "prevten"
@ -12,6 +12,7 @@ class Form::Lettings::Questions::PrevtenRenewal < ::Form::Question
ANSWER_OPTIONS = {
"34" => { "value" => "Specialist retirement housing" },
"36" => { "value" => "Sheltered housing for adults aged under 55 years" },
"35" => { "value" => "Extra care housing" },
"6" => { "value" => "Other supported housing" },
}.freeze

87
app/models/form/lettings/questions/prevten.rb

@ -1,87 +0,0 @@
class Form::Lettings::Questions::Prevten < ::Form::Question
def initialize(id, hsh, page)
super
@id = "prevten"
@check_answer_label = "Where was the household immediately before this letting?"
@header = "Where was the household immediately before this letting?"
@type = "radio"
@check_answers_card_number = 0
@hint_text = "This is where the household was the night before they moved."
@answer_options = ANSWER_OPTIONS
end
ANSWER_OPTIONS = {
"30" => {
"value" => "Fixed-term local authority general needs tenancy",
},
"32" => {
"value" => "Fixed-term private registered provider (PRP) general needs tenancy",
},
"31" => {
"value" => "Lifetime local authority general needs tenancy",
},
"33" => {
"value" => "Lifetime private registered provider (PRP) general needs tenancy",
},
"34" => {
"value" => "Specialist retirement housing",
},
"35" => {
"value" => "Extra care housing",
},
"6" => {
"value" => "Other supported housing",
},
"3" => {
"value" => "Private sector tenancy",
},
"27" => {
"value" => "Owner occupation (low-cost home ownership)",
},
"26" => {
"value" => "Owner occupation (private)",
},
"28" => {
"value" => "Living with friends or family",
},
"14" => {
"value" => "Bed and breakfast",
},
"7" => {
"value" => "Direct access hostel",
},
"10" => {
"value" => "Hospital",
},
"29" => {
"value" => "Prison or approved probation hostel",
},
"19" => {
"value" => "Rough sleeping",
},
"18" => {
"value" => "Any other temporary accommodation",
},
"13" => {
"value" => "Children’s home or foster care",
},
"24" => {
"value" => "Home Office Asylum Support",
},
"23" => {
"value" => "Mobile home or caravan",
},
"21" => {
"value" => "Refuge",
},
"9" => {
"value" => "Residential care home",
},
"4" => {
"value" => "Tied housing or rented with job",
},
"25" => {
"value" => "Any other accommodation",
},
}.freeze
end

3
app/models/form/lettings/questions/rsnvac.rb

@ -36,6 +36,9 @@ class Form::Lettings::Questions::Rsnvac < ::Form::Question
"18" => {
"value" => "Tenant moved to care home",
},
"20" => {
"value" => "Tenant moved to long-stay hospital or similar institution",
},
"6" => {
"value" => "Tenant abandoned property",
},

1
app/models/form/lettings/questions/sheltered.rb

@ -13,6 +13,7 @@ class Form::Lettings::Questions::Sheltered < ::Form::Question
ANSWER_OPTIONS = {
"2" => { "value" => "Yes – extra care housing" },
"1" => { "value" => "Yes – specialist retirement housing" },
"5" => { "value" => "Yes – sheltered housing for adults aged under 55 years" },
"3" => { "value" => "No" },
"divider" => { "value" => true },
"4" => { "value" => "Don’t know" },

1
app/models/form/lettings/questions/voiddate.rb

@ -6,6 +6,5 @@ class Form::Lettings::Questions::Voiddate < ::Form::Question
@header = "What is the void or renewal date?"
@type = "date"
@check_answers_card_number = 0
@hint_text = "For example, 27 3 2021."
end
end

7
app/models/form/lettings/questions/wchair.rb → app/models/form/lettings/questions/wheelchair.rb

@ -1,4 +1,4 @@
class Form::Lettings::Questions::Wchair < ::Form::Question
class Form::Lettings::Questions::Wheelchair < ::Form::Question
def initialize(id, hsh, page)
super
@id = "wchair"
@ -10,5 +10,8 @@ class Form::Lettings::Questions::Wchair < ::Form::Question
@answer_options = ANSWER_OPTIONS
end
ANSWER_OPTIONS = { "1" => { "value" => "Yes" }, "2" => { "value" => "No" } }.freeze
ANSWER_OPTIONS = {
"1" => { "value" => "Yes" },
"2" => { "value" => "No" },
}.freeze
end

7
app/models/form/lettings/subsections/household_characteristics.rb

@ -30,6 +30,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::LeadTenantOverRetirementValueCheck.new(nil, nil, self),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 2),
Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 2),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 2, person_type: "child"),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 2),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 2),
@ -44,6 +45,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 2),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 3),
Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 3),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 3, person_type: "child"),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 3),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 3),
@ -58,6 +60,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 3),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 4),
Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 4),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 4, person_type: "child"),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 4),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 4),
@ -72,6 +75,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 4),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 5),
Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 5),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 5, person_type: "child"),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 5),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 5),
@ -86,6 +90,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 5),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 6),
Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 6),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 6, person_type: "child"),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 6),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 6),
@ -100,6 +105,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 6),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 7),
Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 7),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 7, person_type: "child"),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 7),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 7),
@ -114,6 +120,7 @@ class Form::Lettings::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Lettings::Pages::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 7),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 8),
Form::Lettings::Pages::PersonRelationshipToLead.new(nil, nil, self, person_index: 8),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 8, person_type: "child"),
Form::Lettings::Pages::PersonAge.new(nil, nil, self, person_index: 8),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 8),

2
app/models/form/lettings/subsections/tenancy_information.rb

@ -13,7 +13,7 @@ class Form::Lettings::Subsections::TenancyInformation < ::Form::Subsection
Form::Lettings::Pages::TenancyType.new(nil, nil, self),
Form::Lettings::Pages::StarterTenancyType.new(nil, nil, self),
Form::Lettings::Pages::TenancyLength.new(nil, nil, self),
Form::Lettings::Pages::Shelteredaccom.new(nil, nil, self),
Form::Lettings::Pages::ShelteredAccommodation.new(nil, nil, self),
].compact
end
end

9
app/models/form/sales/pages/about_staircase.rb

@ -1,17 +1,18 @@
class Form::Sales::Pages::AboutStaircase < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "about_staircasing"
def initialize(id, hsh, subsection, joint_purchase:)
super(id, hsh, subsection)
@joint_purchase = joint_purchase
@header = "About the staircasing transaction"
@depends_on = [{
"staircase" => 1,
"joint_purchase?" => joint_purchase,
}]
end
def questions
@questions ||= [
Form::Sales::Questions::StaircaseBought.new(nil, nil, self),
Form::Sales::Questions::StaircaseOwned.new(nil, nil, self),
Form::Sales::Questions::StaircaseOwned.new(nil, nil, self, joint_purchase: @joint_purchase),
staircase_sale_question,
].compact
end

9
app/models/form/sales/pages/buyer_previous.rb

@ -1,12 +1,13 @@
class Form::Sales::Pages::BuyerPrevious < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "buyer_previous"
def initialize(id, hsh, subsection, joint_purchase:)
super(id, hsh, subsection)
@joint_purchase = joint_purchase
@depends_on = [{ "joint_purchase?" => joint_purchase }]
end
def questions
@questions ||= [
Form::Sales::Questions::BuyerPrevious.new(nil, nil, self),
Form::Sales::Questions::BuyerPrevious.new(nil, nil, self, joint_purchase: @joint_purchase),
]
end
end

9
app/models/form/sales/pages/housing_benefits.rb

@ -1,12 +1,13 @@
class Form::Sales::Pages::HousingBenefits < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "housing_benefits"
def initialize(id, hsh, subsection, joint_purchase:)
super(id, hsh, subsection)
@joint_purchase = joint_purchase
@depends_on = [{ "jointpur" => @joint_purchase ? 1 : 2 }]
end
def questions
@questions ||= [
Form::Sales::Questions::HousingBenefits.new(nil, nil, self),
Form::Sales::Questions::HousingBenefits.new(nil, nil, self, joint_purchase: @joint_purchase),
]
end
end

10
app/models/form/sales/pages/number_of_others_in_property.rb

@ -1,20 +1,22 @@
class Form::Sales::Pages::NumberOfOthersInProperty < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "number_of_others_in_property"
def initialize(id, hsh, subsection, joint_purchase:)
super(id, hsh, subsection)
@depends_on = [
{
"privacynotice" => 1,
"jointpur" => joint_purchase ? 1 : 2,
},
{
"noint" => 1,
"jointpur" => joint_purchase ? 1 : 2,
},
]
@joint_purchase = joint_purchase
end
def questions
@questions ||= [
Form::Sales::Questions::NumberOfOthersInProperty.new(nil, nil, self),
Form::Sales::Questions::NumberOfOthersInProperty.new(nil, nil, self, joint_purchase: @joint_purchase),
]
end
end

9
app/models/form/sales/pages/previous_ownership.rb

@ -1,12 +1,13 @@
class Form::Sales::Pages::PreviousOwnership < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "previous_ownership"
def initialize(id, hsh, subsection, joint_purchase:)
super(id, hsh, subsection)
@joint_purchase = joint_purchase
@depends_on = [{ "joint_purchase?" => @joint_purchase }]
end
def questions
@questions ||= [
Form::Sales::Questions::Prevown.new(nil, nil, self),
Form::Sales::Questions::Prevown.new(nil, nil, self, joint_purchase: @joint_purchase),
]
end
end

3
app/models/form/sales/pages/privacy_notice.rb

@ -3,9 +3,6 @@ class Form::Sales::Pages::PrivacyNotice < ::Form::Page
super
@id = "privacy_notice"
@header = "Department for Levelling Up, Housing and Communities privacy notice"
@depends_on = [{
"noint" => 2,
}]
end
def questions

12
app/models/form/sales/questions/buyer1_mortgage.rb

@ -2,8 +2,8 @@ class Form::Sales::Questions::Buyer1Mortgage < ::Form::Question
def initialize(id, hsh, page)
super
@id = "inc1mort"
@check_answer_label = "Buyer 1's income used for mortgage application"
@header = "Was buyer 1's income used for a mortgage application?"
@check_answer_label = "Buyer 1s income used for mortgage application"
@header = "Was buyer 1s income used for a mortgage application?"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@check_answers_card_number = 1
@ -12,5 +12,13 @@ class Form::Sales::Questions::Buyer1Mortgage < ::Form::Question
ANSWER_OPTIONS = {
"1" => { "value" => "Yes" },
"2" => { "value" => "No" },
"3" => { "value" => "Don’t know" },
}.freeze
def displayed_answer_options(_log, _user = nil)
{
"1" => { "value" => "Yes" },
"2" => { "value" => "No" },
}
end
end

29
app/models/form/sales/questions/buyer1_previous_tenure.rb

@ -2,20 +2,23 @@ class Form::Sales::Questions::Buyer1PreviousTenure < ::Form::Question
def initialize(id, hsh, page)
super
@id = "prevten"
@check_answer_label = "Buyer 1's previous tenure"
@header = "What was buyer 1's previous tenure?"
@check_answer_label = "Buyer 1s previous tenure"
@header = "What was buyer 1s previous tenure?"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@answer_options = answer_options
end
ANSWER_OPTIONS = {
"1" => { "value" => "Local Authority" },
"2" => { "value" => "Private registered provider or housing association tenant" },
"3" => { "value" => "Private tenant" },
"5" => { "value" => "Owner occupier" },
"4" => { "value" => "Tied home or renting with job" },
"6" => { "value" => "Living with family or friends" },
"7" => { "value" => "Temporary accomodation" },
"9" => { "value" => "Other" },
}.freeze
def answer_options
{
"1" => { "value" => "Local Authority" },
"2" => { "value" => "Private registered provider or housing association tenant" },
"3" => { "value" => "Private tenant" },
"5" => { "value" => "Owner occupier" },
"4" => { "value" => "Tied home or renting with job" },
"6" => { "value" => "Living with family or friends" },
"7" => { "value" => "Temporary accomodation" },
"9" => { "value" => "Other" },
"0" => { "value" => "Don’t know" },
}
end
end

12
app/models/form/sales/questions/buyer2_mortgage.rb

@ -2,8 +2,8 @@ class Form::Sales::Questions::Buyer2Mortgage < ::Form::Question
def initialize(id, hsh, page)
super
@id = "inc2mort"
@check_answer_label = "Buyer 2's income used for mortgage application"
@header = "Was buyer 2's income used for a mortgage application?"
@check_answer_label = "Buyer 2s income used for mortgage application"
@header = "Was buyer 2s income used for a mortgage application?"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@check_answers_card_number = 2
@ -12,5 +12,13 @@ class Form::Sales::Questions::Buyer2Mortgage < ::Form::Question
ANSWER_OPTIONS = {
"1" => { "value" => "Yes" },
"2" => { "value" => "No" },
"3" => { "value" => "Don’t know" },
}.freeze
def displayed_answer_options(_log, _user = nil)
{
"1" => { "value" => "Yes" },
"2" => { "value" => "No" },
}
end
end

8
app/models/form/sales/questions/buyer_previous.rb

@ -1,9 +1,9 @@
class Form::Sales::Questions::BuyerPrevious < ::Form::Question
def initialize(id, hsh, page)
super
def initialize(id, hsh, page, joint_purchase:)
super(id, hsh, page)
@id = "soctenant"
@check_answer_label = "Buyer was a registered provider, housing association or local authority tenant immediately before this sale?"
@header = "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?"
@check_answer_label = I18n.t("check_answer_labels.soctenant", count: joint_purchase ? 2 : 1)
@header = I18n.t("questions.soctenant", count: joint_purchase ? 2 : 1)
@type = "radio"
@answer_options = ANSWER_OPTIONS
end

10
app/models/form/sales/questions/buyers_organisations.rb

@ -14,8 +14,18 @@ class Form::Sales::Questions::BuyersOrganisations < ::Form::Question
"pregother" => { "value" => "Other private registered provider (PRP) - housing association" },
"pregla" => { "value" => "Local Authority" },
"pregghb" => { "value" => "Help to Buy Agent" },
"pregblank" => { "value" => "None of the above" },
}.freeze
def displayed_answer_options(_log, _user = nil)
{
"pregyrha" => { "value" => "Their private registered provider (PRP) - housing association" },
"pregother" => { "value" => "Other private registered provider (PRP) - housing association" },
"pregla" => { "value" => "Local Authority" },
"pregghb" => { "value" => "Help to Buy Agent" },
}
end
def unanswered_error_message
"At least one option must be selected of these four"
end

6
app/models/form/sales/questions/housing_benefits.rb

@ -1,9 +1,9 @@
class Form::Sales::Questions::HousingBenefits < ::Form::Question
def initialize(id, hsh, page)
super
def initialize(id, hsh, page, joint_purchase:)
super(id, hsh, page)
@id = "hb"
@check_answer_label = "Housing-related benefits buyer received before buying this property"
@header = "Was the buyer receiving any of these housing-related benefits immediately before buying this property?"
@header = "#{joint_purchase ? 'Were the buyers' : 'Was the buyer'} receiving any of these housing-related benefits immediately before buying this property?"
@type = "radio"
@answer_options = ANSWER_OPTIONS
end

8
app/models/form/sales/questions/mortgageused.rb

@ -11,5 +11,13 @@ class Form::Sales::Questions::Mortgageused < ::Form::Question
ANSWER_OPTIONS = {
"1" => { "value" => "Yes" },
"2" => { "value" => "No" },
"3" => { "value" => "Don’t know" },
}.freeze
def displayed_answer_options(_log, _user = nil)
{
"1" => { "value" => "Yes" },
"2" => { "value" => "No" },
}
end
end

18
app/models/form/sales/questions/number_of_others_in_property.rb

@ -1,13 +1,23 @@
class Form::Sales::Questions::NumberOfOthersInProperty < ::Form::Question
def initialize(id, hsh, page)
super
def initialize(id, hsh, page, joint_purchase:)
super(id, hsh, page)
@id = "hholdcount"
@check_answer_label = "Number of other people living in the property"
@header = "Besides the buyer(s), how many other people live or will live in the property?"
@type = "numeric"
@hint_text = "You can provide details for a maximum of 4 other people."
@hint_text = hint(joint_purchase)
@width = 2
@min = 0
@max = 4
@max = joint_purchase ? 4 : 5
end
private
def hint(joint_purchase)
if joint_purchase
"You can provide details for a maximum of 4 other people for a joint purchase."
else
"You can provide details for a maximum of 5 other people if there is only one buyer."
end
end
end

8
app/models/form/sales/questions/prevown.rb

@ -1,9 +1,9 @@
class Form::Sales::Questions::Prevown < ::Form::Question
def initialize(id, hsh, page)
super
def initialize(id, hsh, page, joint_purchase:)
super(id, hsh, page)
@id = "prevown"
@check_answer_label = "Buyers previously owned a property"
@header = "Has the buyer previously owned a property?"
@check_answer_label = I18n.t("check_answer_labels.prevown", count: joint_purchase ? 2 : 1)
@header = I18n.t("questions.prevown", count: joint_purchase ? 2 : 1)
@type = "radio"
@answer_options = ANSWER_OPTIONS
end

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

@ -3,7 +3,7 @@ class Form::Sales::Questions::PropertyLocalAuthorityKnown < ::Form::Question
super
@id = "la_known"
@check_answer_label = "Local authority known"
@header = "Do you know the local authority of the property?"
@header = "Do you know the property’s local authority?"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@conditional_for = { "la" => [1] }

35
app/models/form/sales/questions/shared_ownership_type.rb

@ -6,16 +6,31 @@ class Form::Sales::Questions::SharedOwnershipType < ::Form::Question
@header = "What is the type of shared ownership sale?"
@hint_text = "A shared ownership sale is when the purchaser buys up to 75% of the property value and pays rent to the Private Registered Provider (PRP) on the remaining portion"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@answer_options = answer_options
end
ANSWER_OPTIONS = {
"2" => { "value" => "Shared Ownership" },
"24" => { "value" => "Old Persons Shared Ownership" },
"18" => { "value" => "Social HomeBuy (shared ownership purchase)" },
"16" => { "value" => "Home Ownership for people with Long Term Disabilities (HOLD)" },
"28" => { "value" => "Rent to Buy - Shared Ownership" },
"31" => { "value" => "Right to Shared Ownership" },
"30" => { "value" => "Shared Ownership - 2021 model lease" },
}.freeze
def answer_options
if form.start_date.year >= 2023
{
"2" => { "value" => "Shared Ownership (old model lease)" },
"30" => { "value" => "Shared Ownership (new model lease)" },
"18" => { "value" => "Social HomeBuy — shared ownership purchase" },
"16" => { "value" => "Home Ownership for people with Long-Term Disabilities (HOLD)" },
"24" => { "value" => "Older Persons Shared Ownership" },
"28" => { "value" => "Rent to Buy — Shared Ownership" },
"31" => { "value" => "Right to Shared Ownership (RtSO)" },
"32" => { "value" => "London Living Rent — Shared Ownership" },
}
else
{
"2" => { "value" => "Shared Ownership" },
"24" => { "value" => "Old Persons Shared Ownership" },
"18" => { "value" => "Social HomeBuy (shared ownership purchase)" },
"16" => { "value" => "Home Ownership for people with Long-Term Disabilities (HOLD)" },
"28" => { "value" => "Rent to Buy - Shared Ownership" },
"31" => { "value" => "Right to Shared Ownership" },
"30" => { "value" => "Shared Ownership - 2021 model lease" },
}
end
end
end

8
app/models/form/sales/questions/staircase_owned.rb

@ -1,9 +1,9 @@
class Form::Sales::Questions::StaircaseOwned < ::Form::Question
def initialize(id, hsh, page)
super
def initialize(id, hsh, page, joint_purchase:)
super(id, hsh, page)
@id = "stairowned"
@check_answer_label = "Percentage the buyer now owns in total"
@header = "What percentage of the property does the buyer now own in total?"
@check_answer_label = I18n.t("check_answer_labels.stairowned", count: joint_purchase ? 2 : 1)
@header = I18n.t("questions.stairowned", count: joint_purchase ? 2 : 1)
@type = "numeric"
@width = 5
@min = 0

17
app/models/form/sales/subsections/household_characteristics.rb

@ -3,7 +3,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
super
@id = "household_characteristics"
@label = "Household characteristics"
@depends_on = [{ "setup_completed?" => true }]
@depends_on = [{ "setup_completed?" => true, "company_buyer?" => false }]
end
def pages
@ -37,7 +37,8 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::RetirementValueCheck.new("working_situation_2_retirement_value_check_joint_purchase", nil, self, person_index: 2),
Form::Sales::Pages::Buyer2IncomeValueCheck.new("working_situation_buyer_2_income_value_check", nil, self),
Form::Sales::Pages::Buyer2LiveInProperty.new(nil, nil, self),
Form::Sales::Pages::NumberOfOthersInProperty.new(nil, nil, self),
Form::Sales::Pages::NumberOfOthersInProperty.new("number_of_others_in_property", nil, self, joint_purchase: false),
Form::Sales::Pages::NumberOfOthersInProperty.new("number_of_others_in_property_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::PersonKnown.new("person_2_known", nil, self, person_index: 2),
Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_2_relationship_to_buyer_1", nil, self, person_index: 2),
Form::Sales::Pages::PersonAge.new("person_2_age", nil, self, person_index: 2),
@ -70,6 +71,14 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::RetirementValueCheck.new("gender_5_retirement_value_check", nil, self, person_index: 5),
Form::Sales::Pages::PersonWorkingSituation.new("person_5_working_situation", nil, self, person_index: 5),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_5_retirement_value_check", nil, self, person_index: 5),
Form::Sales::Pages::PersonKnown.new("person_6_known", nil, self, person_index: 6),
Form::Sales::Pages::PersonRelationshipToBuyer1.new("person_6_relationship_to_buyer_1", nil, self, person_index: 6),
Form::Sales::Pages::PersonAge.new("person_6_age", nil, self, person_index: 6),
Form::Sales::Pages::RetirementValueCheck.new("age_6_retirement_value_check", nil, self, person_index: 6),
Form::Sales::Pages::PersonGenderIdentity.new("person_6_gender_identity", nil, self, person_index: 6),
Form::Sales::Pages::RetirementValueCheck.new("gender_6_retirement_value_check", nil, self, person_index: 6),
Form::Sales::Pages::PersonWorkingSituation.new("person_6_working_situation", nil, self, person_index: 6),
Form::Sales::Pages::RetirementValueCheck.new("working_situation_6_retirement_value_check", nil, self, person_index: 6),
].flatten.compact
end
@ -83,4 +92,8 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::Buyer2EthnicBackgroundWhite.new(nil, nil, self)]
end
end
def displayed_in_tasklist?(log)
!log.company_buyer?
end
end

6
app/models/form/sales/subsections/income_benefits_and_savings.rb

@ -18,11 +18,13 @@ class Form::Sales::Subsections::IncomeBenefitsAndSavings < ::Form::Subsection
Form::Sales::Pages::Buyer2IncomeValueCheck.new("buyer_2_income_value_check", nil, self),
Form::Sales::Pages::Buyer2Mortgage.new(nil, nil, self),
Form::Sales::Pages::MortgageValueCheck.new("buyer_2_mortgage_value_check", nil, self, 2),
Form::Sales::Pages::HousingBenefits.new(nil, nil, self),
Form::Sales::Pages::HousingBenefits.new("housing_benefits_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::HousingBenefits.new("housing_benefits_not_joint_purchase", nil, self, joint_purchase: false),
Form::Sales::Pages::Savings.new(nil, nil, self),
Form::Sales::Pages::SavingsValueCheck.new("savings_value_check", nil, self),
Form::Sales::Pages::DepositValueCheck.new("savings_deposit_value_check", nil, self),
Form::Sales::Pages::PreviousOwnership.new(nil, nil, self),
Form::Sales::Pages::PreviousOwnership.new("previous_ownership_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::PreviousOwnership.new("previous_ownership_not_joint_purchase", nil, self, joint_purchase: false),
previous_shared_page,
].compact
end

10
app/models/form/sales/subsections/outright_sale.rb

@ -20,12 +20,18 @@ class Form::Sales::Subsections::OutrightSale < ::Form::Subsection
Form::Sales::Pages::ExtraBorrowing.new("extra_borrowing_outright_sale", nil, self),
Form::Sales::Pages::AboutDepositWithoutDiscount.new("about_deposit_outright_sale", nil, self),
Form::Sales::Pages::DepositValueCheck.new("outright_sale_deposit_value_check", nil, self),
Form::Sales::Pages::LeaseholdCharges.new("leasehold_charges_outright_sale", nil, self),
leasehold_charge_pages,
Form::Sales::Pages::MonthlyChargesValueCheck.new("monthly_charges_outright_sale_value_check", nil, self),
]
].compact
end
def displayed_in_tasklist?(log)
log.ownershipsch == 3
end
def leasehold_charge_pages
if form.start_date.year >= 2023
Form::Sales::Pages::LeaseholdCharges.new("leasehold_charges_outright_sale", nil, self)
end
end
end

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

@ -10,14 +10,16 @@ class Form::Sales::Subsections::SharedOwnershipScheme < ::Form::Subsection
@pages ||= [
Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership", nil, self),
Form::Sales::Pages::Staircase.new(nil, nil, self),
Form::Sales::Pages::AboutStaircase.new(nil, nil, self),
Form::Sales::Pages::AboutStaircase.new("about_staircasing_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::AboutStaircase.new("about_staircasing_not_joint_purchase", nil, self, joint_purchase: false),
Form::Sales::Pages::StaircaseBoughtValueCheck.new(nil, nil, self),
Form::Sales::Pages::Resale.new(nil, nil, self),
Form::Sales::Pages::ExchangeDate.new(nil, nil, self),
Form::Sales::Pages::HandoverDate.new(nil, nil, self),
Form::Sales::Pages::HandoverDateCheck.new(nil, nil, self),
Form::Sales::Pages::LaNominations.new(nil, nil, self),
Form::Sales::Pages::BuyerPrevious.new(nil, nil, self),
Form::Sales::Pages::BuyerPrevious.new("buyer_previous_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::BuyerPrevious.new("buyer_previous_not_joint_purchase", nil, self, joint_purchase: false),
Form::Sales::Pages::PreviousBedrooms.new(nil, nil, self),
Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self),
Form::Sales::Pages::PreviousTenure.new(nil, nil, self),

4
app/models/forms/bulk_upload_lettings/prepare_your_file.rb

@ -29,6 +29,10 @@ module Forms
"/files/bulk-upload-lettings-template-v1.xlsx"
end
def specification_path
"/files/bulk-upload-lettings-specification-2022-23.xlsx"
end
def year_combo
"#{year}/#{year + 1 - 2000}"
end

4
app/models/forms/bulk_upload_sales/prepare_your_file.rb

@ -27,6 +27,10 @@ module Forms
"/files/bulk-upload-sales-template-v1.xlsx"
end
def specification_path
"/files/bulk-upload-sales-specification-2022-23.xlsx"
end
def year_combo
"#{year}/#{year + 1 - 2000}"
end

7
app/models/lettings_log.rb

@ -40,7 +40,6 @@ class LettingsLog < Log
scope :filter_by_year, ->(year) { where(startdate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) }
scope :filter_by_tenant_code, ->(tenant_code) { where("tenancycode ILIKE ?", "%#{tenant_code}%") }
scope :filter_by_propcode, ->(propcode) { where("propcode ILIKE ?", "%#{propcode}%") }
scope :filter_by_postcode, ->(postcode_full) { where("REPLACE(postcode_full, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") }
scope :filter_by_location_postcode, ->(postcode_full) { left_joins(:location).where("REPLACE(locations.postcode, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") }
scope :search_by, lambda { |param|
filter_by_location_postcode(param)
@ -65,11 +64,6 @@ class LettingsLog < Log
FormHandler.instance.get_form(form_name) || FormHandler.instance.current_lettings_form
end
def recalculate_start_year!
@start_year = nil
collection_start_year
end
def lettings?
true
end
@ -518,6 +512,7 @@ private
def reset_derived_questions
dependent_questions = { waityear: [{ key: :renewal, value: 0 }],
referral: [{ key: :renewal, value: 0 }],
rsnvac: [{ key: :renewal, value: 0 }],
underoccupation_benefitcap: [{ key: :renewal, value: 0 }],
wchair: [{ key: :needstype, value: 1 }],
location_id: [{ key: :needstype, value: 1 }] }

2
app/models/local_authority.rb

@ -0,0 +1,2 @@
class LocalAuthority < ApplicationRecord
end

12
app/models/log.rb

@ -18,6 +18,7 @@ class Log < ApplicationRecord
years.each { |year| query = query.or(filter_by_year(year)) }
query.all
}
scope :filter_by_postcode, ->(postcode_full) { where("REPLACE(postcode_full, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") }
scope :filter_by_id, ->(id) { where(id:) }
scope :filter_by_user, lambda { |selected_user, user|
if !selected_user.include?("all") && user.present?
@ -39,6 +40,11 @@ class Log < ApplicationRecord
@start_year = startdate < window_end_date ? startdate.year - 1 : startdate.year
end
def recalculate_start_year!
@start_year = nil
collection_start_year
end
def lettings?
false
end
@ -75,6 +81,12 @@ class Log < ApplicationRecord
end
end
(2..8).each do |person_num|
define_method("person_#{person_num}_child_relation?") do
send("relat#{person_num}") == "C"
end
end
private
def plural_gender_for_person(person_num)

27
app/models/sales_log.rb

@ -24,6 +24,7 @@ class SalesLog < Log
has_paper_trail
validates_with SalesLogValidator
before_validation :recalculate_start_year!, if: :saledate_changed?
before_validation :reset_invalidated_dependent_fields!
before_validation :process_postcode_changes!, if: :postcode_full_changed?
before_validation :process_previous_postcode_changes!, if: :ppostcode_full_changed?
@ -32,7 +33,12 @@ class SalesLog < Log
before_validation :set_derived_fields!
scope :filter_by_year, ->(year) { where(saledate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) }
scope :search_by, ->(param) { filter_by_id(param) }
scope :filter_by_purchaser_code, ->(purchid) { where("purchid ILIKE ?", "%#{purchid}%") }
scope :search_by, lambda { |param|
filter_by_purchaser_code(param)
.or(filter_by_postcode(param))
.or(filter_by_id(param))
}
scope :filter_by_organisation, ->(org, _user = nil) { where(owning_organisation: org) }
OPTIONAL_FIELDS = %w[saledate_check purchid monthly_charges_value_check old_persons_shared_ownership_value_check].freeze
@ -65,7 +71,20 @@ class SalesLog < Log
end
def optional_fields
OPTIONAL_FIELDS
OPTIONAL_FIELDS + dynamically_not_required
end
def dynamically_not_required
not_required = []
not_required << "proplen" if proplen_optional?
not_required
end
def proplen_optional?
return false unless collection_start_year
collection_start_year < 2023
end
def not_started?
@ -243,6 +262,10 @@ class SalesLog < Log
ownershipsch == 1
end
def company_buyer?
companybuy == 1
end
def buyers_age_for_old_persons_shared_ownership_invalid?
return unless old_persons_shared_ownership?

10
app/models/validations/financial_validations.rb

@ -24,11 +24,11 @@ module Validations::FinancialValidations
def validate_net_income(record)
if record.ecstat1 && record.weekly_net_income
if record.weekly_net_income > record.applicable_income_range.hard_max
record.errors.add :earnings, I18n.t("validations.financial.earnings.over_hard_max", hard_max: record.applicable_income_range.hard_max)
record.errors.add :earnings, :over_hard_max, message: I18n.t("validations.financial.earnings.over_hard_max", hard_max: record.applicable_income_range.hard_max)
end
if record.weekly_net_income < record.applicable_income_range.hard_min
record.errors.add :earnings, I18n.t("validations.financial.earnings.under_hard_min", hard_min: record.applicable_income_range.hard_min)
record.errors.add :earnings, :under_hard_min, message: I18n.t("validations.financial.earnings.under_hard_min", hard_min: record.applicable_income_range.hard_min)
end
end
@ -111,9 +111,11 @@ module Validations::FinancialValidations
def validate_care_home_charges(record)
if record.is_carehome?
period = record.form.get_question("period", record).label_from_value(record.period).downcase
# NOTE: This is a temporary change to allow `ccharge` values despite `is_carehome` being true. This value
# is going to be moved to a soft validation in CLDC-2074, so we can safely do this.
if record.chcharge.blank?
record.errors.add :is_carehome, I18n.t("validations.financial.carehome.not_provided", period:)
record.errors.add :chcharge, I18n.t("validations.financial.carehome.not_provided", period:)
# record.errors.add :is_carehome, I18n.t("validations.financial.carehome.not_provided", period:)
# record.errors.add :chcharge, I18n.t("validations.financial.carehome.not_provided", period:)
elsif !weekly_value_in_range(record, "chcharge", 10, 1000)
max_chcharge = record.weekly_to_value_per_period(1000)
min_chcharge = record.weekly_to_value_per_period(10)

2
app/models/validations/household_validations.rb

@ -67,7 +67,7 @@ module Validations::HouseholdValidations
end
if record.age1.present? && record.age1 > 19 && record.previous_tenancy_was_foster_care?
record.errors.add :prevten, I18n.t("validations.household.prevten.over_20_foster_care")
record.errors.add :prevten, :over_20_foster_care, message: I18n.t("validations.household.prevten.over_20_foster_care")
record.errors.add :age1, I18n.t("validations.household.age.lead.over_20")
end

4
app/models/validations/sales/household_validations.rb

@ -14,8 +14,8 @@ module Validations::Sales::HouseholdValidations
return unless record.postcode_full && record.ppostcode_full && record.discounted_ownership_sale?
unless record.postcode_full == record.ppostcode_full
record.errors.add :postcode_full, I18n.t("validations.household.postcode.discounted_ownership")
record.errors.add :ppostcode_full, I18n.t("validations.household.postcode.discounted_ownership")
record.errors.add :postcode_full, :postcodes_not_matching, message: I18n.t("validations.household.postcode.discounted_ownership")
record.errors.add :ppostcode_full, :postcodes_not_matching, message: I18n.t("validations.household.postcode.discounted_ownership")
end
end

2
app/models/validations/sales/sale_information_validations.rb

@ -46,7 +46,7 @@ module Validations::Sales::SaleInformationValidations
def validate_discounted_ownership_value(record)
return unless record.value && record.deposit && record.ownershipsch
return unless record.mortgage || record.mortgageused == 2
return unless record.mortgage || record.mortgageused == 2 || record.mortgageused == 3
return unless record.discount || record.grant || record.type == 29
discount_amount = record.discount ? record.value * record.discount / 100 : 0

2
app/models/validations/sales/soft_validations.rb

@ -60,7 +60,7 @@ module Validations::Sales::SoftValidations
end
def shared_ownership_deposit_invalid?
return unless mortgage || mortgageused == 2
return unless mortgage || mortgageused == 2 || mortgageused == 3
return unless cashdis || !is_type_discount?
return unless deposit && value && equity

1
app/services/bulk_upload/lettings/csv_parser.rb

@ -52,6 +52,7 @@ private
@normalised_string = File.read(path, encoding: "bom|utf-8")
@normalised_string.gsub!("\r\n", "\n")
@normalised_string.scrub!("")
@normalised_string
end

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

@ -16,14 +16,14 @@ class BulkUpload::Lettings::RowParser
attribute :field_9, :integer
attribute :field_10, :string
attribute :field_11, :integer
attribute :field_12, :integer
attribute :field_13, :integer
attribute :field_14, :integer
attribute :field_15, :integer
attribute :field_16, :integer
attribute :field_17, :integer
attribute :field_18, :integer
attribute :field_19, :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
@ -143,6 +143,20 @@ class BulkUpload::Lettings::RowParser
validates :field_1, presence: { message: I18n.t("validations.not_answered", question: "letting type") },
inclusion: { in: (1..12).to_a, message: I18n.t("validations.invalid_option", question: "letting type") }
validates :field_4, presence: { if: proc { [2, 4, 6, 8, 10, 12].include?(field_1) } }
validates :field_12, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 1 must be a number or the letter R" }
validates :field_13, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 2 must be a number or the letter R" }
validates :field_14, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 3 must be a number or the letter R" }
validates :field_15, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 4 must be a number or the letter R" }
validates :field_16, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 5 must be a number or the letter R" }
validates :field_17, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 6 must be a number or the letter R" }
validates :field_18, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 7 must be a number or the letter R" }
validates :field_19, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 8 must be a number or the letter R" }
validates :field_96, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (day)") }
validates :field_97, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (month)") }
validates :field_98, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (year)") }
validates :field_98, format: { with: /\A\d{2}\z/, message: I18n.t("validations.setup.startdate.year_not_two_digits") }
validate :validate_data_types
@ -160,15 +174,19 @@ class BulkUpload::Lettings::RowParser
validate :validate_owning_org_permitted
validate :validate_owning_org_owns_stock
validate :validate_owning_org_exists
validate :validate_owning_org_data_given
validate :validate_managing_org_related
validate :validate_managing_org_exists
validate :validate_managing_org_data_given
validate :validate_scheme_related
validate :validate_scheme_exists
validate :validate_scheme_data_given
validate :validate_location_related
validate :validate_location_exists
validate :validate_location_data_given
def valid?
errors.clear
@ -181,7 +199,12 @@ class BulkUpload::Lettings::RowParser
log.errors.each do |error|
fields = field_mapping_for_errors[error.attribute] || []
fields.each { |field| errors.add(field, error.type) }
fields.each do |field|
unless errors.include?(field)
errors.add(field, error.type)
end
end
end
errors.blank?
@ -226,6 +249,12 @@ private
end
end
def validate_location_data_given
if bulk_upload.supported_housing? && field_5.blank?
errors.add(:field_5, "The scheme code must be present", category: "setup")
end
end
def validate_scheme_related
return unless field_4.present? && scheme.present?
@ -244,6 +273,12 @@ private
end
end
def validate_scheme_data_given
if bulk_upload.supported_housing? && field_4.blank?
errors.add(:field_4, "The management group code is not correct", category: "setup")
end
end
def validate_managing_org_related
if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation)
block_log_creation!
@ -258,6 +293,12 @@ private
end
end
def validate_managing_org_data_given
if field_113.blank?
errors.add(:field_113, "The managing organisation code is incorrect", category: :setup)
end
end
def validate_owning_org_owns_stock
if owning_organisation && !owning_organisation.holds_own_stock?
block_log_creation!
@ -273,6 +314,12 @@ private
end
end
def validate_owning_org_data_given
if field_111.blank?
errors.add(:field_111, "The owning organisation code is incorrect", category: :setup)
end
end
def validate_owning_org_permitted
if owning_organisation && !bulk_upload.user.organisation.affiliated_stock_owners.include?(owning_organisation)
block_log_creation!
@ -337,7 +384,7 @@ private
end
def validate_relevant_collection_window
return unless start_date && bulk_upload.form
return if start_date.blank? || bulk_upload.form.blank?
unless bulk_upload.form.valid_start_date_for_form?(start_date)
errors.add(:field_96, I18n.t("validations.date.outside_collection_window"))
@ -347,6 +394,8 @@ private
end
def start_date
return if field_98.blank? || field_97.blank? || field_96.blank?
Date.parse("20#{field_98.to_s.rjust(2, '0')}-#{field_97}-#{field_96}")
rescue StandardError
nil
@ -387,10 +436,26 @@ private
next if log.optional_fields.include?(question.id)
next if question.completed?(log)
fields.each { |field| errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase)) }
if setup_question?(question)
fields.each do |field|
if errors[field].present?
errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase), category: :setup)
end
end
else
fields.each do |field|
unless errors.any? { |e| fields.include?(e.attribute) }
errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase))
end
end
end
end
end
def setup_question?(question)
log.form.setup_sections[0].subsections[0].questions.include?(question)
end
def field_mapping_for_errors
{
lettype: [:field_1],
@ -398,6 +463,8 @@ private
postcode_known: %i[field_107 field_108 field_109],
postcode_full: %i[field_107 field_108 field_109],
la: %i[field_107],
owning_organisation: [:field_111],
managing_organisation: [:field_113],
owning_organisation_id: [:field_111],
managing_organisation_id: [:field_113],
renewal: [:field_134],
@ -627,22 +694,29 @@ private
attributes["tenancylength"] = field_11
attributes["declaration"] = field_132
attributes["age1_known"] = field_12.present? ? 0 : 1
attributes["age1"] = field_12
attributes["age2_known"] = field_13.present? ? 0 : 1
attributes["age2"] = field_13
attributes["age3_known"] = field_14.present? ? 0 : 1
attributes["age3"] = field_14
attributes["age4_known"] = field_15.present? ? 0 : 1
attributes["age4"] = field_15
attributes["age5_known"] = field_16.present? ? 0 : 1
attributes["age5"] = field_16
attributes["age6_known"] = field_17.present? ? 0 : 1
attributes["age6"] = field_17
attributes["age7_known"] = field_18.present? ? 0 : 1
attributes["age7"] = field_18
attributes["age8_known"] = field_19.present? ? 0 : 1
attributes["age8"] = field_19
attributes["age1_known"] = field_12 == "R" ? 1 : 0
attributes["age1"] = field_12 if attributes["age1_known"].zero? && field_12&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age2_known"] = field_13 == "R" ? 1 : 0
attributes["age2"] = field_13 if attributes["age2_known"].zero? && field_13&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age3_known"] = field_14 == "R" ? 1 : 0
attributes["age3"] = field_14 if attributes["age3_known"].zero? && field_14&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age4_known"] = field_15 == "R" ? 1 : 0
attributes["age4"] = field_15 if attributes["age4_known"].zero? && field_15&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age5_known"] = field_16 == "R" ? 1 : 0
attributes["age5"] = field_16 if attributes["age5_known"].zero? && field_16&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age6_known"] = field_17 == "R" ? 1 : 0
attributes["age6"] = field_17 if attributes["age6_known"].zero? && field_17&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age7_known"] = field_18 == "R" ? 1 : 0
attributes["age7"] = field_18 if attributes["age7_known"].zero? && field_18&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age8_known"] = field_19 == "R" ? 1 : 0
attributes["age8"] = field_19 if attributes["age8_known"].zero? && field_19&.match(/\A\d{1,3}\z|\AR\z/)
attributes["sex1"] = field_20
attributes["sex2"] = field_21
@ -861,6 +935,8 @@ private
0
when nil
rsnvac == 14 ? 1 : 0
else
field_134
end
end

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

@ -168,13 +168,14 @@ class BulkUpload::Lettings::Validator
row:,
cell: "#{cols[field_number_for_attribute(error.attribute) - col_offset + 1]}#{row}",
col: cols[field_number_for_attribute(error.attribute) - col_offset + 1],
category: error.options[:category],
)
end
end
end
def create_logs?
return false if any_setup_sections_incomplete?
return false if any_setup_errors?
return false if over_column_error_threshold?
return false if row_parsers.any?(&:block_log_creation?)
@ -185,12 +186,16 @@ class BulkUpload::Lettings::Validator
QUESTIONS[field]
end
private
def any_setup_sections_incomplete?
row_parsers.any? { |row_parser| row_parser.log.form.setup_sections[0].subsections[0].is_incomplete?(row_parser.log) }
def any_setup_errors?
bulk_upload
.bulk_upload_errors
.where(category: "setup")
.count
.positive?
end
private
def over_column_error_threshold?
fields = ("field_1".."field_134").to_a
percentage_threshold = (row_parsers.size * COLUMN_PERCENTAGE_ERROR_THRESHOLD).ceil

36
app/services/bulk_upload/processor.rb

@ -12,11 +12,15 @@ class BulkUpload::Processor
validator.call
create_logs if validator.create_logs?
send_correct_and_upload_again_mail unless validator.create_logs?
send_fix_errors_mail if created_logs_but_incompleted?
send_success_mail if created_logs_and_all_completed?
if validator.any_setup_errors?
send_setup_errors_mail
elsif validator.create_logs?
create_logs
send_fix_errors_mail if created_logs_but_incompleted?
send_success_mail if created_logs_and_all_completed?
else
send_correct_and_upload_again_mail
end
rescue StandardError => e
Sentry.capture_exception(e)
send_failure_mail
@ -26,16 +30,28 @@ class BulkUpload::Processor
private
def send_setup_errors_mail
BulkUploadMailer
.send_bulk_upload_failed_file_setup_error_mail(bulk_upload:)
.deliver_later
end
def send_correct_and_upload_again_mail
BulkUploadMailer.send_correct_and_upload_again_mail(bulk_upload:).deliver_later
BulkUploadMailer
.send_correct_and_upload_again_mail(bulk_upload:)
.deliver_later
end
def send_fix_errors_mail
BulkUploadMailer.send_bulk_upload_with_errors_mail(bulk_upload:).deliver_later
BulkUploadMailer
.send_bulk_upload_with_errors_mail(bulk_upload:)
.deliver_later
end
def send_success_mail
BulkUploadMailer.send_bulk_upload_complete_mail(user:, bulk_upload:).deliver_later
BulkUploadMailer
.send_bulk_upload_complete_mail(user:, bulk_upload:)
.deliver_later
end
def created_logs_but_incompleted?
@ -47,7 +63,9 @@ private
end
def send_failure_mail
BulkUploadMailer.send_bulk_upload_failed_service_error_mail(bulk_upload:).deliver_later
BulkUploadMailer
.send_bulk_upload_failed_service_error_mail(bulk_upload:)
.deliver_later
end
def user

145
app/services/imports/lettings_logs_import_service.rb

@ -1,5 +1,5 @@
module Imports
class LettingsLogsImportService < ImportService
class LettingsLogsImportService < LogsImportService
def initialize(storage_service, logger = Rails.logger)
@logs_with_discrepancies = Set.new
@logs_overridden = Set.new
@ -55,6 +55,8 @@ module Imports
}.freeze
def create_log(xml_doc)
return if meta_field_value(xml_doc, "form-name").include?("Sales")
attributes = {}
previous_status = meta_field_value(xml_doc, "status")
@ -287,8 +289,38 @@ module Imports
@logs_overridden << lettings_log.old_id
attributes.delete("referral")
save_lettings_log(attributes, previous_status)
elsif lettings_log.errors.of_kind?(:earnings, :under_hard_min)
@logger.warn("Log #{lettings_log.old_id}: Where the income is 0, set earnings and income to blank and set incref to refused")
@logs_overridden << lettings_log.old_id
attributes.delete("earnings")
attributes.delete("incfreq")
attributes["incref"] = 1
attributes["net_income_known"] = 2
save_lettings_log(attributes, previous_status)
elsif lettings_log.errors.include?(:tenancylength) && lettings_log.errors.include?(:tenancy)
@logger.warn("Log #{lettings_log.old_id}: Removing tenancylength as invalid")
@logs_overridden << lettings_log.old_id
attributes.delete("tenancylength")
attributes.delete("tenancy")
save_lettings_log(attributes, previous_status)
elsif lettings_log.errors.of_kind?(:prevten, :over_20_foster_care)
@logger.warn("Log #{lettings_log.old_id}: Removing age1 and prevten as incompatible")
@logs_overridden << lettings_log.old_id
attributes.delete("prevten")
attributes.delete("age1")
save_lettings_log(attributes, previous_status)
else
@logger.error("Log #{lettings_log.old_id}: Failed to import")
lettings_log.errors.each do |error|
@logger.error("Validation error: Field #{error.attribute}:")
@logger.error("\tOwning Organisation: #{lettings_log.owning_organisation&.name}")
@logger.error("\tManaging Organisation: #{lettings_log.managing_organisation&.name}")
@logger.error("\tOld CORE ID: #{lettings_log.old_id}")
@logger.error("\tOld CORE: #{attributes[error.attribute.to_s]&.inspect}")
@logger.error("\tNew CORE: #{lettings_log.read_attribute(error.attribute)&.inspect}")
@logger.error("\tError message: #{error.type}")
end
raise exception
end
end
@ -318,43 +350,6 @@ module Imports
end
end
# Safe: A string that represents only an integer (or empty/nil)
def safe_string_as_integer(xml_doc, attribute)
str = field_value(xml_doc, "xmlns", attribute)
Integer(str, exception: false)
end
# Safe: A string that represents only a decimal (or empty/nil)
def safe_string_as_decimal(xml_doc, attribute)
str = string_or_nil(xml_doc, attribute)
if str.nil?
nil
else
BigDecimal(str, exception: false)
end
end
# Unsafe: A string that has more than just the integer value
def unsafe_string_as_integer(xml_doc, attribute)
str = string_or_nil(xml_doc, attribute)
if str.nil?
nil
else
str.to_i
end
end
def compose_date(xml_doc, day_str, month_str, year_str)
day = Integer(field_value(xml_doc, "xmlns", day_str), exception: false)
month = Integer(field_value(xml_doc, "xmlns", month_str), exception: false)
year = Integer(field_value(xml_doc, "xmlns", year_str), exception: false)
if day.nil? || month.nil? || year.nil?
nil
else
Time.zone.local(year, month, day)
end
end
def get_form_name_component(xml_doc, index)
form_name = meta_field_value(xml_doc, "form-name")
form_type_components = form_name.split("-")
@ -399,42 +394,6 @@ module Imports
end
end
def find_organisation_id(xml_doc, id_field)
old_visible_id = string_or_nil(xml_doc, id_field)
organisation = Organisation.find_by(old_visible_id:)
raise "Organisation not found with legacy ID #{old_visible_id}" if organisation.nil?
organisation.id
end
def sex(xml_doc, index)
sex = string_or_nil(xml_doc, "P#{index}Sex")
case sex
when "Male"
"M"
when "Female"
"F"
when "Other", "Non-binary"
"X"
when "Refused"
"R"
end
end
def relat(xml_doc, index)
relat = string_or_nil(xml_doc, "P#{index}Rel")
case relat
when "Child"
"C"
when "Partner"
"P"
when "Other", "Non-binary"
"X"
when "Refused"
"R"
end
end
def age_known(xml_doc, index, hhmemb)
return nil if hhmemb.present? && index > hhmemb
@ -473,16 +432,6 @@ module Imports
end
end
def compose_postcode(xml_doc, outcode, incode)
outcode_value = string_or_nil(xml_doc, outcode)
incode_value = string_or_nil(xml_doc, incode)
if outcode_value.nil? || incode_value.nil? || !"#{outcode_value} #{incode_value}".match(POSTCODE_REGEXP)
nil
else
"#{outcode_value} #{incode_value}"
end
end
def london_affordable_rent(xml_doc)
lar = unsafe_string_as_integer(xml_doc, "LAR")
if lar == 1
@ -502,34 +451,6 @@ module Imports
end
end
def string_or_nil(xml_doc, attribute)
str = field_value(xml_doc, "xmlns", attribute)
str.presence
end
def ethnic_group(ethnic)
case ethnic
when 1, 2, 3, 18
# White
0
when 4, 5, 6, 7
# Mixed
1
when 8, 9, 10, 11, 15
# Asian
2
when 12, 13, 14
# Black
3
when 16, 19
# Others
4
when 17
# Refused
17
end
end
# Letters should be lowercase to match case
def housing_needs(xml_doc, letter)
housing_need = string_or_nil(xml_doc, "Q10-#{letter}")

25
app/services/imports/local_authorities_service.rb

@ -0,0 +1,25 @@
require "csv"
module Imports
class LocalAuthoritiesService
attr_reader :path, :count
def initialize(path:)
@path = path
@count = 0
end
def call
CSV.foreach(path, headers: true) do |row|
LocalAuthority.upsert(
{ code: row["code"],
name: row["name"],
start_date: Time.zone.local(row["start_year"], 4, 1),
end_date: (Time.zone.local(row["end_year"], 3, 31) if row["end_year"]) },
unique_by: %i[code],
)
@count += 1
end
end
end
end

127
app/services/imports/logs_import_service.rb

@ -0,0 +1,127 @@
module Imports
class LogsImportService < ImportService
private
# Safe: A string that represents only an integer (or empty/nil)
def safe_string_as_integer(xml_doc, attribute)
str = field_value(xml_doc, "xmlns", attribute)
Integer(str, exception: false)
end
# Unsafe: A string that has more than just the integer value
def unsafe_string_as_integer(xml_doc, attribute)
str = string_or_nil(xml_doc, attribute)
if str.nil?
nil
else
str.to_i
end
end
def compose_date(xml_doc, day_str, month_str, year_str)
day = Integer(field_value(xml_doc, "xmlns", day_str), exception: false)
month = Integer(field_value(xml_doc, "xmlns", month_str), exception: false)
year = Integer(field_value(xml_doc, "xmlns", year_str), exception: false)
if day.nil? || month.nil? || year.nil?
nil
else
Time.zone.local(year, month, day)
end
end
def find_organisation_id(xml_doc, id_field)
old_visible_id = string_or_nil(xml_doc, id_field)
organisation = Organisation.find_by(old_visible_id:)
raise "Organisation not found with legacy ID #{old_visible_id}" if organisation.nil?
organisation.id
end
def string_or_nil(xml_doc, attribute)
str = field_value(xml_doc, "xmlns", attribute)
str.presence
end
def ethnic_group(ethnic)
case ethnic
when 1, 2, 3, 18
# White
0
when 4, 5, 6, 7
# Mixed
1
when 8, 9, 10, 11, 15
# Asian
2
when 12, 13, 14
# Black
3
when 16, 19
# Others
4
when 17
# Refused
17
end
end
# Safe: A string that represents only a decimal (or empty/nil)
def safe_string_as_decimal(xml_doc, attribute)
str = string_or_nil(xml_doc, attribute)
if str.nil?
nil
else
BigDecimal(str, exception: false)
end
end
def compose_postcode(xml_doc, outcode, incode)
outcode_value = string_or_nil(xml_doc, outcode)
incode_value = string_or_nil(xml_doc, incode)
if outcode_value.nil? || incode_value.nil? || !"#{outcode_value} #{incode_value}".match(POSTCODE_REGEXP)
nil
else
"#{outcode_value} #{incode_value}"
end
end
def previous_postcode_known(xml_doc, previous_postcode, prevloc)
previous_postcode_known = string_or_nil(xml_doc, "Q7UnknownPostcode")
if previous_postcode_known == "If postcode not known tick" || (previous_postcode.nil? && prevloc.present?)
1
elsif previous_postcode.nil?
nil
else
0
end
end
def sex(xml_doc, index)
sex = string_or_nil(xml_doc, "P#{index}Sex")
case sex
when "Male"
"M"
when "Female"
"F"
when "Other", "Non-binary"
"X"
when "Refused"
"R"
end
end
def relat(xml_doc, index)
relat = string_or_nil(xml_doc, "P#{index}Rel")
case relat
when "Child"
"C"
when "Partner"
"P"
when "Other", "Non-binary"
"X"
when "Refused", "Buyer prefers not to say"
"R"
end
end
end
end

549
app/services/imports/sales_logs_import_service.rb

@ -0,0 +1,549 @@
module Imports
class SalesLogsImportService < LogsImportService
def initialize(storage_service, logger = Rails.logger)
@logs_with_discrepancies = Set.new
@logs_overridden = Set.new
super
end
def create_logs(folder)
import_from(folder, :create_log)
if @logs_with_discrepancies.count.positive?
@logger.warn("The following sales logs had status discrepancies: [#{@logs_with_discrepancies.join(', ')}]")
end
end
private
def create_log(xml_doc)
# only import sales logs from 22/23 collection period onwards
return unless meta_field_value(xml_doc, "form-name").include?("Sales")
return unless compose_date(xml_doc, "DAY", "MONTH", "YEAR") >= Time.zone.local(2022, 4, 1)
attributes = {}
previous_status = meta_field_value(xml_doc, "status")
# Required fields for status complete or logic to work
# Note: order matters when we derive from previous values (attributes parameter)
attributes["saledate"] = compose_date(xml_doc, "DAY", "MONTH", "YEAR")
attributes["owning_organisation_id"] = find_organisation_id(xml_doc, "OWNINGORGID")
attributes["type"] = unsafe_string_as_integer(xml_doc, "DerSaleType")
attributes["old_id"] = meta_field_value(xml_doc, "document-id")
attributes["created_at"] = Time.zone.parse(meta_field_value(xml_doc, "created-date"))
attributes["updated_at"] = Time.zone.parse(meta_field_value(xml_doc, "modified-date"))
attributes["purchid"] = string_or_nil(xml_doc, "PurchaserCode")
attributes["ownershipsch"] = unsafe_string_as_integer(xml_doc, "Ownership")
attributes["ownershipsch"] = ownership_from_type(attributes) if attributes["ownershipsch"].blank? # sometimes Ownership is missing, but type is set
attributes["othtype"] = string_or_nil(xml_doc, "Q38OtherSale")
attributes["jointpur"] = unsafe_string_as_integer(xml_doc, "joint")
attributes["jointmore"] = unsafe_string_as_integer(xml_doc, "JointMore") if attributes["jointpur"] == 1
attributes["beds"] = safe_string_as_integer(xml_doc, "Q11Bedrooms")
attributes["companybuy"] = unsafe_string_as_integer(xml_doc, "company") if attributes["ownershipsch"] == 3
attributes["hholdcount"] = other_household_members(xml_doc, attributes)
attributes["hhmemb"] = household_members(xml_doc, attributes)
(1..6).each do |index|
attributes["age#{index}"] = safe_string_as_integer(xml_doc, "P#{index}Age")
attributes["sex#{index}"] = sex(xml_doc, index)
attributes["ecstat#{index}"] = unsafe_string_as_integer(xml_doc, "P#{index}Eco")
attributes["age#{index}_known"] = age_known(xml_doc, index, attributes["hhmemb"], attributes["age#{index}"])
end
(2..6).each do |index|
attributes["relat#{index}"] = relat(xml_doc, index)
attributes["details_known_#{index}"] = details_known(index, attributes)
end
attributes["national"] = unsafe_string_as_integer(xml_doc, "P1Nat")
attributes["othernational"] = nil
attributes["ethnic"] = unsafe_string_as_integer(xml_doc, "P1Eth")
attributes["ethnic_group"] = ethnic_group(attributes["ethnic"])
attributes["buy1livein"] = unsafe_string_as_integer(xml_doc, "LiveInBuyer1")
attributes["buylivein"] = unsafe_string_as_integer(xml_doc, "LiveInBuyer") if attributes["ownershipsch"] == 3
attributes["builtype"] = unsafe_string_as_integer(xml_doc, "Q13BuildingType")
attributes["proptype"] = unsafe_string_as_integer(xml_doc, "Q12PropertyType")
attributes["privacynotice"] = 1 if string_or_nil(xml_doc, "Qdp") == "Yes"
attributes["noint"] = unsafe_string_as_integer(xml_doc, "PartAPurchaser")
attributes["buy2livein"] = unsafe_string_as_integer(xml_doc, "LiveInBuyer2")
attributes["wheel"] = unsafe_string_as_integer(xml_doc, "Q10Wheelchair")
attributes["la"] = string_or_nil(xml_doc, "Q14ONSLACode")
attributes["income1"] = safe_string_as_integer(xml_doc, "Q2Person1Income")
attributes["income1nk"] = income_known(unsafe_string_as_integer(xml_doc, "P1IncKnown"))
attributes["inc1mort"] = unsafe_string_as_integer(xml_doc, "Q2Person1Mortgage")
attributes["income2"] = safe_string_as_integer(xml_doc, "Q2Person2Income")
attributes["income2nk"] = income_known(unsafe_string_as_integer(xml_doc, "P2IncKnown"))
attributes["savings"] = safe_string_as_integer(xml_doc, "Q3Savings")
attributes["savingsnk"] = savings_known(xml_doc)
attributes["prevown"] = unsafe_string_as_integer(xml_doc, "Q4PrevOwnedProperty")
attributes["mortgage"] = safe_string_as_decimal(xml_doc, "CALCMORT")
attributes["inc2mort"] = unsafe_string_as_integer(xml_doc, "Q2Person2MortApplication")
attributes["hb"] = unsafe_string_as_integer(xml_doc, "Q2a")
attributes["frombeds"] = safe_string_as_integer(xml_doc, "Q20Bedrooms")
attributes["staircase"] = unsafe_string_as_integer(xml_doc, "Q17aStaircase") if attributes["ownershipsch"] == 1
attributes["stairbought"] = safe_string_as_integer(xml_doc, "PercentBought")
attributes["stairowned"] = safe_string_as_integer(xml_doc, "PercentOwns") if attributes["staircase"] == 1
attributes["mrent"] = safe_string_as_decimal(xml_doc, "Q28MonthlyRent")
attributes["exdate"] = compose_date(xml_doc, "EXDAY", "EXMONTH", "EXYEAR")
attributes["exday"] = safe_string_as_integer(xml_doc, "EXDAY")
attributes["exmonth"] = safe_string_as_integer(xml_doc, "EXMONTH")
attributes["exyear"] = safe_string_as_integer(xml_doc, "EXYEAR")
attributes["resale"] = unsafe_string_as_integer(xml_doc, "Q17Resale")
attributes["deposit"] = deposit(xml_doc, attributes)
attributes["cashdis"] = safe_string_as_decimal(xml_doc, "Q27SocialHomeBuy")
attributes["disabled"] = unsafe_string_as_integer(xml_doc, "Disability")
attributes["lanomagr"] = unsafe_string_as_integer(xml_doc, "Q19Rehoused")
attributes["value"] = purchase_price(xml_doc, attributes)
attributes["equity"] = safe_string_as_decimal(xml_doc, "Q23Equity")
attributes["discount"] = safe_string_as_decimal(xml_doc, "Q33Discount")
attributes["grant"] = safe_string_as_decimal(xml_doc, "Q32Reductions")
attributes["pregyrha"] = 1 if string_or_nil(xml_doc, "PREGYRHA") == "Yes"
attributes["pregla"] = 1 if string_or_nil(xml_doc, "PREGLA") == "Yes"
attributes["pregghb"] = 1 if string_or_nil(xml_doc, "PREGHBA") == "Yes"
attributes["pregother"] = 1 if string_or_nil(xml_doc, "PREGOTHER") == "Yes"
attributes["ppostcode_full"] = parse_postcode(string_or_nil(xml_doc, "Q7Postcode"))
attributes["prevloc"] = string_or_nil(xml_doc, "Q7ONSLACode")
attributes["ppcodenk"] = previous_postcode_known(xml_doc, attributes["ppostcode_full"], attributes["prevloc"]) # Q7UNKNOWNPOSTCODE check mapping
attributes["ppostc1"] = string_or_nil(xml_doc, "PPOSTC1")
attributes["ppostc2"] = string_or_nil(xml_doc, "PPOSTC2")
attributes["hhregres"] = unsafe_string_as_integer(xml_doc, "ArmedF")
attributes["hhregresstill"] = still_serving(xml_doc)
attributes["proplen"] = safe_string_as_integer(xml_doc, "Q16aProplen2") || safe_string_as_integer(xml_doc, "Q16aProplensec2")
attributes["mscharge"] = monthly_charges(xml_doc, attributes)
attributes["mscharge_known"] = 1 if attributes["mscharge"].present?
attributes["prevten"] = unsafe_string_as_integer(xml_doc, "Q6PrevTenure")
attributes["mortlen"] = mortgage_length(xml_doc, attributes)
attributes["extrabor"] = borrowing(xml_doc, attributes)
attributes["mortgageused"] = mortgage_used(xml_doc, attributes)
attributes["wchair"] = unsafe_string_as_integer(xml_doc, "Q15Wheelchair")
attributes["armedforcesspouse"] = unsafe_string_as_integer(xml_doc, "ARMEDFORCESSPOUSE")
attributes["hodate"] = compose_date(xml_doc, "HODAY", "HOMONTH", "HOYEAR")
attributes["hoday"] = safe_string_as_integer(xml_doc, "HODAY")
attributes["homonth"] = safe_string_as_integer(xml_doc, "HOMONTH")
attributes["hoyear"] = safe_string_as_integer(xml_doc, "HOYEAR")
attributes["fromprop"] = unsafe_string_as_integer(xml_doc, "Q21PropertyType")
attributes["socprevten"] = unsafe_string_as_integer(xml_doc, "PrevRentType")
attributes["mortgagelender"] = mortgage_lender(xml_doc, attributes)
attributes["mortgagelenderother"] = mortgage_lender_other(xml_doc, attributes)
attributes["postcode_full"] = parse_postcode(string_or_nil(xml_doc, "Q14Postcode"))
attributes["pcodenk"] = 0 if attributes["postcode_full"].present? # known if given
attributes["soctenant"] = soctenant(attributes)
attributes["ethnic_group2"] = nil # 23/24 variable
attributes["ethnicbuy2"] = nil # 23/24 variable
attributes["prevshared"] = nil # 23/24 variable
attributes["staircasesale"] = nil # 23/24 variable
attributes["previous_la_known"] = 1 if attributes["prevloc"].present?
if attributes["la"].present?
attributes["la_known"] = 1
attributes["is_la_inferred"] = false
end
# Soft validations can become required answers, set them to yes by default
attributes["mortgage_value_check"] = 0
attributes["shared_ownership_deposit_value_check"] = 0
attributes["value_value_check"] = 0
attributes["savings_value_check"] = 0
attributes["income1_value_check"] = 0
attributes["deposit_value_check"] = 0
attributes["wheel_value_check"] = 0
attributes["retirement_value_check"] = 0
attributes["extrabor_value_check"] = 0
attributes["grant_value_check"] = 0
attributes["staircase_bought_value_check"] = 0
attributes["deposit_and_mortgage_value_check"] = 0
attributes["old_persons_shared_ownership_value_check"] = 0
attributes["income2_value_check"] = 0
attributes["monthly_charges_value_check"] = 0
# Sets the log creator
owner_id = meta_field_value(xml_doc, "owner-user-id").strip
if owner_id.present?
user = LegacyUser.find_by(old_user_id: owner_id)&.user
@logger.warn "Missing user! We expected to find a legacy user with old_user_id #{owner_id}" unless user
attributes["created_by"] = user
end
set_default_values(attributes) if previous_status.include?("submitted")
sales_log = save_sales_log(attributes, previous_status)
compute_differences(sales_log, attributes)
check_status_completed(sales_log, previous_status) unless @logs_overridden.include?(sales_log.old_id)
end
def save_sales_log(attributes, previous_status)
sales_log = SalesLog.new(attributes)
begin
sales_log.save!
sales_log
rescue ActiveRecord::RecordNotUnique
legacy_id = attributes["old_id"]
record = SalesLog.find_by(old_id: legacy_id)
@logger.info "Updating sales log #{record.id} with legacy ID #{legacy_id}"
record.update!(attributes)
record
rescue ActiveRecord::RecordInvalid => e
rescue_validation_or_raise(sales_log, attributes, previous_status, e)
end
end
def rescue_validation_or_raise(sales_log, attributes, previous_status, exception)
if %w[saved submitted-invalid].include?(previous_status)
sales_log.errors.each do |error|
@logger.warn("Log #{sales_log.old_id}: Removing field #{error.attribute} from log triggering validation: #{error.type}")
attributes.delete(error.attribute.to_s)
end
@logs_overridden << sales_log.old_id
if sales_log.errors.of_kind?(:postcode_full, :postcodes_not_matching)
@logger.warn("Log #{sales_log.old_id}: Removing postcode known and previous postcode known as the postcodes are invalid")
attributes.delete("pcodenk")
attributes.delete("ppcodenk")
end
save_sales_log(attributes, previous_status)
else
@logger.error("Log #{sales_log.old_id}: Failed to import")
sales_log.errors.each do |error|
@logger.error("Validation error: Field #{error.attribute}:")
@logger.error("\tOwning Organisation: #{sales_log.owning_organisation&.name}")
@logger.error("\tOld CORE ID: #{sales_log.old_id}")
@logger.error("\tOld CORE: #{attributes[error.attribute.to_s]&.inspect}")
@logger.error("\tNew CORE: #{sales_log.read_attribute(error.attribute)&.inspect}")
@logger.error("\tError message: #{error.type}")
end
raise exception
end
end
def compute_differences(sales_log, attributes)
differences = []
attributes.each do |key, value|
sales_log_value = sales_log.send(key.to_sym)
next if fields_not_present_in_softwire_data.include?(key)
if value != sales_log_value
differences.push("#{key} #{value.inspect} #{sales_log_value.inspect}")
end
end
@logger.warn "Differences found when saving log #{sales_log.old_id}: #{differences}" unless differences.empty?
end
def fields_not_present_in_softwire_data
%w[created_by
income1_value_check
income2_value_check
mortgage_value_check
savings_value_check
deposit_value_check
wheel_value_check
retirement_value_check
extrabor_value_check
deposit_and_mortgage_value_check
shared_ownership_deposit_value_check
grant_value_check
value_value_check
old_persons_shared_ownership_value_check
staircase_bought_value_check
monthly_charges_value_check
hodate_check
saledate_check]
end
def check_status_completed(sales_log, previous_status)
if previous_status.include?("submitted") && sales_log.status != "completed"
@logger.warn "sales log #{sales_log.id} is not completed. The following answers are missing: #{missing_answers(sales_log).join(', ')}"
@logger.warn "sales log with old id:#{sales_log.old_id} is incomplete but status should be complete"
@logs_with_discrepancies << sales_log.old_id
end
end
def age_known(_xml_doc, index, hhmemb, age)
return nil if hhmemb.present? && index > hhmemb
return 0 if age.present?
end
def details_known(index, attributes)
return nil if attributes["hhmemb"].nil? || index > attributes["hhmemb"]
return nil if attributes["jointpur"] == 1 && index == 2
if attributes["age#{index}_known"] != 0 &&
attributes["sex#{index}"] == "R" &&
attributes["relat#{index}"] == "R" &&
attributes["ecstat#{index}"] == 10
2 # No
else
1 # Yes
end
end
MORTGAGE_LENDER_OPTIONS = {
"atom bank" => 1,
"barclays bank plc" => 2,
"bath building society" => 3,
"buckinghamshire building society" => 4,
"cambridge building society" => 5,
"coventry building society" => 6,
"cumberland building society" => 7,
"darlington building society" => 8,
"dudley building society" => 9,
"ecology building society" => 10,
"halifax" => 11,
"hanley economic building society" => 12,
"hinckley and rugby building society" => 13,
"holmesdale building society" => 14,
"ipswich building society" => 15,
"leeds building society" => 16,
"lloyds bank" => 17,
"mansfield building society" => 18,
"market harborough building society" => 19,
"melton mowbray building society" => 20,
"nationwide building society" => 21,
"natwest" => 22,
"nedbank private wealth" => 23,
"newbury building society" => 24,
"oneSavings bank" => 25,
"parity trust" => 26,
"penrith building society" => 27,
"pepper homeloans" => 28,
"royal bank of scotland" => 29,
"santander" => 30,
"skipton building society" => 31,
"teachers building society" => 32,
"the co-operative bank" => 33,
"tipton & coseley building society" => 34,
"tss" => 35,
"ulster bank" => 36,
"virgin money" => 37,
"west bromwich building society" => 38,
"yorkshire building society" => 39,
"other" => 40,
}.freeze
# this comes through as a string, need to map to a corresponding integer
def mortgage_lender(xml_doc, attributes)
lender = case attributes["ownershipsch"]
when 1
string_or_nil(xml_doc, "Q24aMortgageLender")
when 2
string_or_nil(xml_doc, "Q34a")
when 3
string_or_nil(xml_doc, "Q41aMortgageLender")
end
return if lender.blank?
MORTGAGE_LENDER_OPTIONS[lender.downcase] || MORTGAGE_LENDER_OPTIONS["other"]
end
def mortgage_lender_other(xml_doc, attributes)
return unless attributes["mortgagelender"] == MORTGAGE_LENDER_OPTIONS["other"]
case attributes["ownershipsch"]
when 1
string_or_nil(xml_doc, "Q24aMortgageLender")
when 2
string_or_nil(xml_doc, "Q34a")
when 3
string_or_nil(xml_doc, "Q41aMortgageLender")
end
end
def mortgage_length(xml_doc, attributes)
case attributes["ownershipsch"]
when 1
unsafe_string_as_integer(xml_doc, "Q24b")
when 2
unsafe_string_as_integer(xml_doc, "Q34b")
when 3
unsafe_string_as_integer(xml_doc, "Q41b")
end
end
def savings_known(xml_doc)
case unsafe_string_as_integer(xml_doc, "savingsKnown")
when 1 # known
0
when 2 # unknown
1
end
end
def soctenant(attributes)
return nil unless attributes["ownershipsch"] == 1
if attributes["frombeds"].blank? && attributes["fromprop"].blank? && attributes["socprevten"].blank?
2
else
1
end
# NO (2) if FROMBEDS, FROMPROP and socprevten are blank, and YES(1) if they are completed
end
def still_serving(xml_doc)
case unsafe_string_as_integer(xml_doc, "LeftArmedF")
when 4
4
when 5, 6
5
end
end
def income_known(value)
case value
when 1 # known
0
when 2 # unknown
1
end
end
def borrowing(xml_doc, attributes)
case attributes["ownershipsch"]
when 1
unsafe_string_as_integer(xml_doc, "Q25Borrowing")
when 2
unsafe_string_as_integer(xml_doc, "Q35Borrowing")
when 3
unsafe_string_as_integer(xml_doc, "Q42Borrowing")
end
end
def purchase_price(xml_doc, attributes)
case attributes["ownershipsch"]
when 1
safe_string_as_decimal(xml_doc, "Q22PurchasePrice")
when 2
safe_string_as_decimal(xml_doc, "Q31PurchasePrice")
when 3
safe_string_as_decimal(xml_doc, "Q40PurchasePrice")
end
end
def deposit(xml_doc, attributes)
case attributes["ownershipsch"]
when 1
safe_string_as_decimal(xml_doc, "Q26CashDeposit")
when 2
safe_string_as_decimal(xml_doc, "Q36CashDeposit")
when 3
safe_string_as_decimal(xml_doc, "Q43CashDeposit")
end
end
def monthly_charges(xml_doc, attributes)
case attributes["ownershipsch"]
when 1
safe_string_as_decimal(xml_doc, "Q29MonthlyCharges")
when 2
safe_string_as_decimal(xml_doc, "Q37MonthlyCharges")
end
end
def ownership_from_type(attributes)
case attributes["type"]
when 2, 24, 18, 16, 28, 31, 30
1 # shared ownership
when 8, 14, 27, 9, 29, 21, 22
2 # discounted ownership
when 10, 12
3 # outright sale
end
end
def other_household_members(xml_doc, attributes)
hholdcount = safe_string_as_integer(xml_doc, "LiveInOther")
return hholdcount if hholdcount.present?
other_people_with_details(xml_doc, attributes)
end
def other_people_with_details(xml_doc, attributes)
number_of_buyers = attributes["jointpur"] == 1 ? 2 : 1
highest_person_index_with_details = number_of_buyers
(2..6).each do |person_index|
age = string_or_nil(xml_doc, "P#{person_index}Age")
gender = string_or_nil(xml_doc, "P#{person_index}Sex")
relationship = string_or_nil(xml_doc, "P#{person_index}Rel")
economic_status = string_or_nil(xml_doc, "P#{person_index}Eco")
if gender.present? || age.present? || relationship.present? || economic_status.present?
highest_person_index_with_details = person_index
end
end
highest_person_index_with_details - number_of_buyers
end
def household_members(_xml_doc, attributes)
if attributes["jointpur"] == 2
attributes["hholdcount"] + 1
else
attributes["hholdcount"] + 2
end
end
def parse_postcode(postcode)
return if postcode.blank?
UKPostcode.parse(postcode).to_s
end
def mortgage_used(xml_doc, attributes)
mortgageused = unsafe_string_as_integer(xml_doc, "MORTGAGEUSED")
return mortgageused unless mortgageused == 3
if attributes["mortgage"].present? || attributes["mortlen"].present? || attributes["extrabor"].present?
1 # yes
else
3 # don't know
end
end
def set_default_values(attributes)
attributes["armedforcesspouse"] ||= 7
attributes["hhregres"] ||= 8
attributes["disabled"] ||= 3
attributes["wheel"] ||= 3
attributes["hb"] ||= 4
attributes["prevown"] ||= 3
attributes["savingsnk"] ||= attributes["savings"].present? ? 0 : 1
attributes["jointmore"] ||= 3 if attributes["jointpur"] == 1
attributes["inc1mort"] ||= 3
if [attributes["pregyrha"], attributes["pregla"], attributes["pregghb"], attributes["pregother"]].all?(&:blank?)
attributes["pregblank"] = 1
end
attributes["pcodenk"] ||= 1
attributes["prevten"] ||= 0
# buyer 1 characteristics
attributes["age1_known"] ||= 1
attributes["sex1"] ||= "R"
attributes["ethnic_group"] ||= 17
attributes["ethnic"] ||= 17
attributes["national"] ||= 13
attributes["ecstat1"] ||= 10
attributes["income1nk"] ||= attributes["income1"].present? ? 0 : 1
# buyer 2 characteristics
if attributes["jointpur"] == 1
attributes["age2_known"] ||= 1
attributes["sex2"] ||= "R"
attributes["ecstat2"] ||= 10
attributes["income2nk"] ||= attributes["income2"].present? ? 0 : 1
attributes["relat2"] ||= "R"
attributes["inc2mort"] ||= 3
attributes["buy2livein"] ||= 1 unless attributes["ownershipsch"] == 3
end
# other household members characteristics
(2..attributes["hhmemb"]).each do |index|
attributes["age#{index}_known"] ||= 1
attributes["sex#{index}"] ||= "R"
attributes["ecstat#{index}"] ||= 10
attributes["relat#{index}"] ||= "R"
end
end
def missing_answers(sales_log)
applicable_questions = sales_log.form.subsections.map { |s| s.applicable_questions(sales_log).select { |q| q.enabled?(sales_log) } }.flatten
applicable_questions.filter { |q| q.unanswered?(sales_log) }.map(&:id) - sales_log.optional_fields
end
end
end

2
app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb

@ -13,7 +13,7 @@
<h2 class="govuk-heading-m">Create your file</h2>
<ul class="govuk-list govuk-list--bullet">
<li>Download the <%= govuk_link_to "bulk lettings template", @form.template_path %></li>
<li>Export the data from your housing management system, matching the template</li>
<li>Export the data from your housing management system, matching the template. <%= govuk_link_to "Find out more about exporting your data", guidance_bulk_upload_lettings_logs_path %></li>
<li>If you cannot export it in this format, you may have to input it manually</li>
<li>You can not have a file with both general needs logs and supported housing logs. These must be in separate files</li>
</ul>

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

@ -1,5 +1,7 @@
<% content_for :before_content do %>
<%= govuk_back_link(text: "Back", href: summary_bulk_upload_lettings_result_path(@bulk_upload)) %>
<% if BulkUploadErrorSummaryTableComponent.new(bulk_upload: @bulk_upload).errors? %>
<% content_for :before_content do %>
<%= govuk_back_link(text: "Back", href: summary_bulk_upload_lettings_result_path(@bulk_upload)) %>
<% end %>
<% end %>
<div class="govuk-grid-row">
@ -22,3 +24,5 @@
<% end %>
</div>
</div>
<%= govuk_button_link_to "Upload your file again", start_bulk_upload_lettings_logs_path %>

2
app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb

@ -13,7 +13,7 @@
<h2 class="govuk-heading-m">Create your file</h2>
<ul class="govuk-list govuk-list--bullet">
<li>Download the <%= govuk_link_to "bulk sales template", @form.template_path %></li>
<li>Export the data from your housing management system, matching the template</li>
<li>Export the data from your housing management system, matching the template. <%= govuk_link_to "Find out more about exporting your data", guidance_bulk_upload_sales_logs_path %></li>
<li>If you cannot export it in this format, you may have to input it manually</li>
</ul>

52
app/views/bulk_upload_shared/guidance.html.erb

@ -0,0 +1,52 @@
<% content_for :before_content do %>
<%= govuk_back_link href: :back %>
<% end %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-l">How to upload logs in bulk</h1>
<div class="govuk-!-padding-bottom-4">
<h2 class="govuk-heading-s">Uploading sales and lettings logs</h2>
<p class="govuk-body">You can upload one sales or lettings log at a time, or many at once (known as ‘bulk upload’) with a comma-separated values (CSV) spreadsheet file.</p>
<p class="govuk-body">Bulk upload may be easier if your organisation deals with many logs, or if you can export CSV data from your Housing Management System (HMS). If your organisation only deals with a small amount of logs, or you cannot export CSV data, it’s probably easier to enter logs individually.</p>
<%= govuk_warning_text text: "You cannot upload lettings and sales logs with the same template - you must export each data type separately, then upload them" %>
</div>
<div class="govuk-!-padding-bottom-4">
<h2 class="govuk-heading-s">Creating your CSV files</h2>
<p class="govuk-body">To bulk upload successfully, all spreadsheets must be in the correct CSV format.</p>
<p class="govuk-body">In most programs, you must resave files as CSV - it’s not usually the default setting. CSV files are also unformatted, so any formatting added before saving (for example colours) will automatically disappear.</p>
<%= govuk_details(summary_text: "More about CSV") do %>
<p class="govuk-body">A CSV file is a basic spreadsheet with data values in plain text, and columns separated by commas. Each data row is a new text line.</p>
<p class="govuk-body">CSV data is easier to process than more common advanced spreadsheet formats, for example Excel. It means CSV is well suited to upload large, or multiple data sets.</p>
<% end %>
</div>
<div class="govuk-!-padding-bottom-4">
<h2 class="govuk-heading-s">Exporting CSV data</h2>
<p class="govuk-body">Export CSV data directly from your current systems, or export then adjust it to CSV.</p>
<p class="govuk-body">You can then upload it via a button at the top of the lettings and sales logs pages.</p>
<%= govuk_details(summary_text: "My organisation has a CMS") do %>
<p class="govuk-body">Some HMS providers sell an add-on "eCORE" module, which exports CSV data for you.</p>
<p class="govuk-!-font-weight-bold">It can take HMS providers a while to update these per new collection year, so you may have to wait for updates to export, or adjust your data manually post-export.</p>
<% end %>
<%= govuk_details(summary_text: "My organisation does not have a CMS") do %>
<p class="govuk-body">Your organisation’s IT team may be able to export CSV data for you - <%= govuk_link_to "find out more about data specification", @form.specification_path %>. This document outlines:</p>
<ul class="govuk-list govuk-list--bullet">
<li>required fields</li>
<li>each field's valid response</li>
<li>if/when certain fields can be left blank</li>
</ul>
<p class="govuk-body">Fields can appear in any order, as long as you include the <%= govuk_link_to "template document", @form.template_path %> headers, to easily identify what each column represents. You can rearrange data columns to match your system exports, copy-pasting multiple columns at once. For data stored in multiple systems, you can copy-paste all columns for one system next to each other, repeating this for subsequent system exports.</p>
<% end %>
</div>
<div class="govuk-!-padding-bottom-4">
<h2 class="govuk-heading-s">Getting help</h2>
<p class="govuk-body">There is no step-by-step bulk upload guide like there is with single log upload. However, you can download <%= govuk_link_to "our template", @form.template_path %>, which you can copy-paste data into from your systems column-by-column. You can also view a post-upload report showing any data errors, and our <%= govuk_link_to "data specification", @form.specification_path %> can help fix these.</p>
<p class="govuk-body">If you still need support mapping data in the way we need, DLUHC’s helpdesk can help. If your data is across multiple systems, or is hard to export as a single file in the correct format, you could try different exports, or copy-pasting data by hand.</p>
</div>
</div>
</div>

2
app/views/form/_date_question.html.erb

@ -3,7 +3,7 @@
<%= f.govuk_date_field question.id.to_sym,
caption: caption(caption_text, page_header, conditional),
legend: legend(question, page_header, conditional),
hint: { text: question.hint_text&.html_safe || "For example, 1 9 2022." },
hint: { text: question.hint_text&.html_safe || "For example, #{date_mid_collection_year_formatted(lettings_log.startdate)}" },
width: 20,
**stimulus_html_attributes(question) do %>
<%= govuk_inset_text(text: question.unresolved_hint_text) if question.unresolved_hint_text.present? && @log.unresolved %>

1
app/views/form/headers/_person_6_known_page.erb

@ -0,0 +1 @@
You have given us the details for <%= log.joint_purchase? ? 3 : 4 %> of the <%= log.hholdcount %> other people in the household

4
app/views/logs/index.html.erb

@ -64,9 +64,9 @@
<%= render partial: "log_filters" %>
<div class="app-filter-layout__content">
<%= render SearchComponent.new(current_user:, search_label: "Search by log ID, tenant code, property reference or postcode", value: @searched) %>
<%= render SearchComponent.new(current_user:, search_label: search_label_for_controller(controller), value: @searched) %>
<%= govuk_section_break(visible: true, size: "m") %>
<%= render partial: "log_list", locals: { logs: @logs, title: "Logs", pagy: @pagy, searched: @searched, item_label:, total_count: @total_count, csv_download_url: csv_download_lettings_logs_path(search: @search_term) } %>
<%= render partial: "log_list", locals: { logs: @logs, title: "Logs", pagy: @pagy, searched: @searched, item_label:, total_count: @total_count, csv_download_url: csv_download_url_for_controller(controller) } %>
<%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
</div>
</div>

1
config/initializers/date_formats.rb

@ -2,6 +2,7 @@ Time::DATE_FORMATS[:govuk_date] = "%-d %B %Y"
Time::DATE_FORMATS[:govuk_date_short_month] = "%-d %b %Y"
Date::DATE_FORMATS[:govuk_date] = "%-d %B %Y"
Date::DATE_FORMATS[:govuk_date_short_month] = "%-d %b %Y"
Date::DATE_FORMATS[:govuk_date_number_month] = "%-d %-m %Y"
Time::DATE_FORMATS[:month_and_year] = "%B %Y"
Date::DATE_FORMATS[:month_and_year] = "%B %Y"

2
config/initializers/feature_toggle.rb

@ -17,7 +17,7 @@ class FeatureToggle
end
def self.sales_log_enabled?
!Rails.env.production?
true
end
def self.managing_owning_enabled?

388
config/local_authorities_data/initial_local_authorities.csv

@ -0,0 +1,388 @@
code,name,start_year,end_year
S12000033,Aberdeen City,2021,
S12000034,Aberdeenshire,2021,
E07000223,Adur,2021,
E07000032,Amber Valley,2021,
S12000041,Angus,2021,
N09000001,Antrim and Newtownabbey,2021,
N09000011,Ards and North Down,2021,
S12000035,Argyll and Bute,2021,
N09000002,"Armagh City, Banbridge and Craigavon",2021,
E07000224,Arun,2021,
E07000170,Ashfield,2021,
E07000105,Ashford,2021,
E07000200,Babergh,2021,
E09000002,Barking and Dagenham,2021,
E09000003,Barnet,2021,
E08000016,Barnsley,2021,
E07000066,Basildon,2021,
E07000084,Basingstoke and Deane,2021,
E07000171,Bassetlaw,2021,
E06000022,Bath and North East Somerset,2021,
E06000055,Bedford,2021,
N09000003,Belfast,2021,
E09000004,Bexley,2021,
E08000025,Birmingham,2021,
E07000129,Blaby,2021,
E06000008,Blackburn with Darwen,2021,
E06000009,Blackpool,2021,
W06000019,Blaenau Gwent,2021,
E07000033,Bolsover,2021,
E08000001,Bolton,2021,
E07000136,Boston,2021,
E06000058,"Bournemouth, Christchurch and Poole",2021,
E06000036,Bracknell Forest,2021,
E08000032,Bradford,2021,
E07000067,Braintree,2021,
E07000143,Breckland,2021,
E09000005,Brent,2021,
E07000068,Brentwood,2021,
W06000013,Bridgend,2021,
E06000043,Brighton and Hove,2021,
E06000023,"Bristol, City of",2021,
E07000144,Broadland,2021,
E09000006,Bromley,2021,
E07000234,Bromsgrove,2021,
E07000095,Broxbourne,2021,
E07000172,Broxtowe,2021,
E06000060,Buckinghamshire,2021,
E07000117,Burnley,2021,
E08000002,Bury,2021,
W06000018,Caerphilly,2021,
E08000033,Calderdale,2021,
E07000008,Cambridge,2021,
E09000007,Camden,2021,
E07000192,Cannock Chase,2021,
E07000106,Canterbury,2021,
W06000015,Cardiff,2021,
W06000010,Carmarthenshire,2021,
E07000069,Castle Point,2021,
N09000004,Causeway Coast and Glens,2021,
E06000056,Central Bedfordshire,2021,
W06000008,Ceredigion,2021,
E07000130,Charnwood,2021,
E07000070,Chelmsford,2021,
E07000078,Cheltenham,2021,
E07000177,Cherwell,2021,
E06000049,Cheshire East,2021,
E06000050,Cheshire West and Chester,2021,
E07000034,Chesterfield,2021,
E07000225,Chichester,2021,
E07000118,Chorley,2021,
S12000036,City of Edinburgh,2021,
E09000001,City of London,2021,
S12000005,Clackmannanshire,2021,
E07000071,Colchester,2021,
W06000003,Conwy,2021,
E07000150,Corby,2021,
E06000052,Cornwall,2021,
E07000079,Cotswold,2021,
E06000047,County Durham,2021,
E08000026,Coventry,2021,
E07000226,Crawley,2021,
E09000008,Croydon,2021,
E06000063,Cumberland,2023,
E07000096,Dacorum,2021,
E06000005,Darlington,2021,
E07000107,Dartford,2021,
E07000151,Daventry,2021,
W06000004,Denbighshire,2021,
E06000015,Derby,2021,
E07000035,Derbyshire Dales,2021,
N09000005,Derry City and Strabane,2021,
E08000017,Doncaster,2021,
E06000059,Dorset,2021,
E07000108,Dover,2021,
E08000027,Dudley,2021,
S12000006,Dumfries and Galloway,2021,
S12000042,Dundee City,2021,
E09000009,Ealing,2021,
S12000008,East Ayrshire,2021,
E07000009,East Cambridgeshire,2021,
E07000040,East Devon,2021,
S12000045,East Dunbartonshire,2021,
E07000085,East Hampshire,2021,
E07000242,East Hertfordshire,2021,
E07000137,East Lindsey,2021,
S12000010,East Lothian,2021,
E07000152,East Northamptonshire,2021,
S12000011,East Renfrewshire,2021,
E06000011,East Riding of Yorkshire,2021,
E07000193,East Staffordshire,2021,
E07000244,East Suffolk,2021,
E07000061,Eastbourne,2021,
E07000086,Eastleigh,2021,
E07000207,Elmbridge,2021,
E09000010,Enfield,2021,
E07000072,Epping Forest,2021,
E07000208,Epsom and Ewell,2021,
E07000036,Erewash,2021,
E07000041,Exeter,2021,
S12000014,Falkirk,2021,
E07000087,Fareham,2021,
E07000010,Fenland,2021,
N09000006,Fermanagh and Omagh,2021,
S12000047,Fife,2021,
W06000005,Flintshire,2021,
E07000112,Folkestone and Hythe,2021,
E07000080,Forest of Dean,2021,
E07000119,Fylde,2021,
E08000037,Gateshead,2021,
E07000173,Gedling,2021,
S12000049,Glasgow City,2021,
E07000081,Gloucester,2021,
E07000088,Gosport,2021,
E07000109,Gravesham,2021,
E07000145,Great Yarmouth,2021,
E09000011,Greenwich,2021,
E07000209,Guildford,2021,
W06000002,Gwynedd,2021,
E09000012,Hackney,2021,
E06000006,Halton,2021,
E09000013,Hammersmith and Fulham,2021,
E07000131,Harborough,2021,
E09000014,Haringey,2021,
E07000073,Harlow,2021,
E09000015,Harrow,2021,
E07000089,Hart,2021,
E06000001,Hartlepool,2021,
E07000062,Hastings,2021,
E07000090,Havant,2021,
E09000016,Havering,2021,
E06000019,"Herefordshire, County of",2021,
E07000098,Hertsmere,2021,
E07000037,High Peak,2021,
S12000017,Highland,2021,
E09000017,Hillingdon,2021,
E07000132,Hinckley and Bosworth,2021,
E07000227,Horsham,2021,
E09000018,Hounslow,2021,
E07000011,Huntingdonshire,2021,
E07000120,Hyndburn,2021,
S12000018,Inverclyde,2021,
E07000202,Ipswich,2021,
W06000001,Isle of Anglesey,2021,
E06000046,Isle of Wight,2021,
E06000053,Isles of Scilly,2021,
E09000019,Islington,2021,
E09000020,Kensington and Chelsea,2021,
E07000153,Kettering,2021,
E07000146,King’s Lynn and West Norfolk,2021,
E06000010,"Kingston upon Hull, City of",2021,
E09000021,Kingston upon Thames,2021,
E08000034,Kirklees,2021,
E08000011,Knowsley,2021,
E09000022,Lambeth,2021,
E07000121,Lancaster,2021,
E08000035,Leeds,2021,
E06000016,Leicester,2021,
E07000063,Lewes,2021,
E09000023,Lewisham,2021,
E07000194,Lichfield,2021,
E07000138,Lincoln,2021,
N09000007,Lisburn and Castlereagh,2021,
E08000012,Liverpool,2021,
E06000032,Luton,2021,
E07000110,Maidstone,2021,
E07000074,Maldon,2021,
E07000235,Malvern Hills,2021,
E08000003,Manchester,2021,
E07000174,Mansfield,2021,
E06000035,Medway,2021,
E07000133,Melton,2021,
W06000024,Merthyr Tydfil,2021,
E09000024,Merton,2021,
E07000042,Mid Devon,2021,
E07000203,Mid Suffolk,2021,
E07000228,Mid Sussex,2021,
N09000009,Mid Ulster,2021,
N09000008,Mid and East Antrim,2021,
E06000002,Middlesbrough,2021,
S12000019,Midlothian,2021,
E06000042,Milton Keynes,2021,
E07000210,Mole Valley,2021,
W06000021,Monmouthshire,2021,
S12000020,Moray,2021,
S12000013,Na h-Eileanan Siar,2021,
W06000012,Neath Port Talbot,2021,
E07000091,New Forest,2021,
E07000175,Newark and Sherwood,2021,
E08000021,Newcastle upon Tyne,2021,
E07000195,Newcastle-under-Lyme,2021,
E09000025,Newham,2021,
W06000022,Newport,2021,
N09000010,"Newry, Mourne and Down",2021,
S12000021,North Ayrshire,2021,
E07000043,North Devon,2021,
E07000038,North East Derbyshire,2021,
E06000012,North East Lincolnshire,2021,
E07000099,North Hertfordshire,2021,
E07000139,North Kesteven,2021,
S12000050,North Lanarkshire,2021,
E06000013,North Lincolnshire,2021,
E07000147,North Norfolk,2021,
E06000024,North Somerset,2021,
E08000022,North Tyneside,2021,
E07000218,North Warwickshire,2021,
E07000134,North West Leicestershire,2021,
E07000154,Northampton,2021,
E06000057,Northumberland,2021,
E07000148,Norwich,2021,
E06000018,Nottingham,2021,
E07000219,Nuneaton and Bedworth,2021,
E07000135,Oadby and Wigston,2021,
E08000004,Oldham,2021,
S12000023,Orkney Islands,2021,
E07000178,Oxford,2021,
W06000009,Pembrokeshire,2021,
E07000122,Pendle,2021,
S12000048,Perth and Kinross,2021,
E06000031,Peterborough,2021,
E06000026,Plymouth,2021,
E06000044,Portsmouth,2021,
W06000023,Powys,2021,
E07000123,Preston,2021,
E06000038,Reading,2021,
E09000026,Redbridge,2021,
E06000003,Redcar and Cleveland,2021,
E07000236,Redditch,2021,
E07000211,Reigate and Banstead,2021,
S12000038,Renfrewshire,2021,
W06000016,Rhondda Cynon Taf,2021,
E07000124,Ribble Valley,2021,
E09000027,Richmond upon Thames,2021,
E08000005,Rochdale,2021,
E07000075,Rochford,2021,
E07000125,Rossendale,2021,
E07000064,Rother,2021,
E08000018,Rotherham,2021,
E07000220,Rugby,2021,
E07000212,Runnymede,2021,
E07000176,Rushcliffe,2021,
E07000092,Rushmoor,2021,
E06000017,Rutland,2021,
E08000006,Salford,2021,
E08000028,Sandwell,2021,
S12000026,Scottish Borders,2021,
E08000014,Sefton,2021,
E07000111,Sevenoaks,2021,
E08000019,Sheffield,2021,
S12000027,Shetland Islands,2021,
E06000051,Shropshire,2021,
E06000039,Slough,2021,
E08000029,Solihull,2021,
E06000066,Somerset,2023,
S12000028,South Ayrshire,2021,
E07000012,South Cambridgeshire,2021,
E07000039,South Derbyshire,2021,
E06000025,South Gloucestershire,2021,
E07000044,South Hams,2021,
E07000140,South Holland,2021,
E07000141,South Kesteven,2021,
S12000029,South Lanarkshire,2021,
E07000149,South Norfolk,2021,
E07000155,South Northamptonshire,2021,
E07000179,South Oxfordshire,2021,
E07000126,South Ribble,2021,
E07000196,South Staffordshire,2021,
E08000023,South Tyneside,2021,
E06000045,Southampton,2021,
E06000033,Southend-on-Sea,2021,
E09000028,Southwark,2021,
E07000213,Spelthorne,2021,
E07000240,St Albans,2021,
E08000013,St. Helens,2021,
E07000197,Stafford,2021,
E07000198,Staffordshire Moorlands,2021,
E07000243,Stevenage,2021,
S12000030,Stirling,2021,
E08000007,Stockport,2021,
E06000004,Stockton-on-Tees,2021,
E06000021,Stoke-on-Trent,2021,
E07000221,Stratford-on-Avon,2021,
E07000082,Stroud,2021,
E08000024,Sunderland,2021,
E07000214,Surrey Heath,2021,
E09000029,Sutton,2021,
E07000113,Swale,2021,
W06000011,Swansea,2021,
E06000030,Swindon,2021,
E08000008,Tameside,2021,
E07000199,Tamworth,2021,
E07000215,Tandridge,2021,
E07000045,Teignbridge,2021,
E06000020,Telford and Wrekin,2021,
E07000076,Tendring,2021,
E07000093,Test Valley,2021,
E07000083,Tewkesbury,2021,
E07000114,Thanet,2021,
E07000102,Three Rivers,2021,
E06000034,Thurrock,2021,
E07000115,Tonbridge and Malling,2021,
E06000027,Torbay,2021,
W06000020,Torfaen,2021,
E07000046,Torridge,2021,
E09000030,Tower Hamlets,2021,
E08000009,Trafford,2021,
E07000116,Tunbridge Wells,2021,
E07000077,Uttlesford,2021,
W06000014,Vale of Glamorgan,2021,
E07000180,Vale of White Horse,2021,
E08000036,Wakefield,2021,
E08000030,Walsall,2021,
E09000031,Waltham Forest,2021,
E09000032,Wandsworth,2021,
E06000007,Warrington,2021,
E07000222,Warwick,2021,
E07000103,Watford,2021,
E07000216,Waverley,2021,
E07000065,Wealden,2021,
E07000156,Wellingborough,2021,
E07000241,Welwyn Hatfield,2021,
E06000037,West Berkshire,2021,
E07000047,West Devon,2021,
S12000039,West Dunbartonshire,2021,
E07000127,West Lancashire,2021,
E07000142,West Lindsey,2021,
S12000040,West Lothian,2021,
E07000181,West Oxfordshire,2021,
E07000245,West Suffolk,2021,
E09000033,Westminster,2021,
E06000064,Westmorland and Furness,2023,
E08000010,Wigan,2021,
E06000054,Wiltshire,2021,
E07000094,Winchester,2021,
E06000040,Windsor and Maidenhead,2021,
E08000015,Wirral,2021,
E07000217,Woking,2021,
E06000041,Wokingham,2021,
E08000031,Wolverhampton,2021,
E07000237,Worcester,2021,
E07000229,Worthing,2021,
W06000006,Wrexham,2021,
E07000238,Wychavon,2021,
E07000128,Wyre,2021,
E07000239,Wyre Forest,2021,
E06000014,York,2021,
E06000065,North Yorkshire,2023,
N92000002,Northern Ireland,2021,
S92000003,Scotland,2021,
W92000004,Wales,2021,
9300000XX,Outside UK,2021,
E07000027,Barrow-in-Furness,2021,2023,
E07000030,Eden,2021,2023,
E07000031,South Lakeland,2021,2023,
E07000026,Allerdale,2021,2023,
E07000028,Carlisle,2021,2023,
E07000029,Copeland,2021,2023,
E07000163,Craven,2021,2023,
E07000164,Hambleton,2021,2023,
E07000165,Harrogate,2021,2023,
E07000166,Richmondshire,2021,2023,
E07000167,Ryedale,2021,2023,
E07000168,Scarborough,2021,2023,
E07000169,Selby,2021,2023,
E07000187,Mendip,2021,2023,
E07000188,Sedgemoor,2021,2023,
E07000246,Somerset West and Taunton,2021,2023,
E07000189,South Somerset,2021,2023,
Can't render this file because it has a wrong number of fields in line 372.

20
config/locales/en.yml

@ -541,6 +541,15 @@ en:
W: "Suitable for someone who uses a wheelchair and offers the full use of all rooms and facilities."
A: "Fitted with stairlifts, ramps, level access showers or grab rails."
N: "Not designed to wheelchair-user standards or fitted with any equipment or adaptations."
soctenant:
one: "Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?"
other: "Were any of the buyers private registered providers, housing association or local authority tenants immediately before this sale?"
prevown:
one: "Has the buyer previously owned a property?"
other: "Have any of the buyers previously owned a property?"
stairowned:
one: "What percentage of the property does the buyer now own in total?"
other: "What percentage of the property do the buyers now own in total?"
hints:
location:
@ -554,6 +563,17 @@ en:
bulk_upload:
needstype: "General needs housing includes both self-contained and shared housing without support or specific adaptations. Supported housing can include direct access hostels, group homes, residential care and nursing homes."
check_answer_labels:
soctenant:
one: "Buyer was a registered provider, housing association or local authority tenant immediately before this sale?"
other: "Any buyers were registered providers, housing association or local authority tenants immediately before this sale?"
prevown:
one: "Buyer previously owned a property"
other: "Buyers previously owned a property"
stairowned:
one: "Percentage the buyer now owns in total"
other: "Percentage the buyers now own in total"
warnings:
location:
deactivate:

2
config/routes.rb

@ -131,6 +131,7 @@ Rails.application.routes.draw do
resources :bulk_upload_lettings_logs, path: "bulk-upload-logs", only: %i[show update] do
collection do
get :start
get "guidance", to: "bulk_upload_lettings_logs#guidance"
end
end
@ -165,6 +166,7 @@ Rails.application.routes.draw do
resources :bulk_upload_sales_logs, path: "bulk-upload-logs" do
collection do
get :start
get "guidance", to: "bulk_upload_sales_logs#guidance"
end
end

32
config/sale_range_data/2023.csv

@ -87,10 +87,10 @@ Boston,E07000136,1,58000,258000
Boston,E07000136,2,58000,258000
Boston,E07000136,3,111000,308000
Boston,E07000136,4,212000,510000
Bournemouth, Christchurch and Poole,E06000058,1,140000,500000
Bournemouth, Christchurch and Poole,E06000058,2,197000,500000
Bournemouth, Christchurch and Poole,E06000058,3,285000,657000
Bournemouth, Christchurch and Poole,E06000058,4,370000,1321000
"Bournemouth, Christchurch and Poole",E06000058,1,140000,500000
"Bournemouth, Christchurch and Poole",E06000058,2,197000,500000
"Bournemouth, Christchurch and Poole",E06000058,3,285000,657000
"Bournemouth, Christchurch and Poole",E06000058,4,370000,1321000
Bracknell Forest,E06000036,1,95000,488000
Bracknell Forest,E06000036,2,157000,488000
Bracknell Forest,E06000036,3,341000,654000
@ -119,10 +119,10 @@ Brighton and Hove,E06000043,1,185000,526000
Brighton and Hove,E06000043,2,270000,610000
Brighton and Hove,E06000043,3,350000,846000
Brighton and Hove,E06000043,4,458000,1487000
Bristol, City of,E06000023,1,145000,417000
Bristol, City of,E06000023,2,184000,562000
Bristol, City of,E06000023,3,242000,685000
Bristol, City of,E06000023,4,331000,1394000
"Bristol, City of",E06000023,1,145000,417000
"Bristol, City of",E06000023,2,184000,562000
"Bristol, City of",E06000023,3,242000,685000
"Bristol, City of",E06000023,4,331000,1394000
Broadland,E07000144,1,126000,334000
Broadland,E07000144,2,140000,334000
Broadland,E07000144,3,225000,433000
@ -459,10 +459,10 @@ Havering,E09000016,1,137000,472000
Havering,E09000016,2,204000,481000
Havering,E09000016,3,336000,657000
Havering,E09000016,4,412000,1232000
Herefordshire, County of,E06000019,1,98000,419000
Herefordshire, County of,E06000019,2,105000,419000
Herefordshire, County of,E06000019,3,162000,499000
Herefordshire, County of,E06000019,4,283000,885000
"Herefordshire, County of",E06000019,1,98000,419000
"Herefordshire, County of",E06000019,2,105000,419000
"Herefordshire, County of",E06000019,3,162000,499000
"Herefordshire, County of",E06000019,4,283000,885000
Hertsmere,E07000098,1,178000,666000
Hertsmere,E07000098,2,316000,666000
Hertsmere,E07000098,3,440000,918000
@ -515,10 +515,10 @@ King's Lynn and West Norfolk,E07000146,1,77000,346000
King's Lynn and West Norfolk,E07000146,2,123000,346000
King's Lynn and West Norfolk,E07000146,3,161000,408000
King's Lynn and West Norfolk,E07000146,4,243000,778000
Kingston upon Hull, City of,E06000010,1,63000,189000
Kingston upon Hull, City of,E06000010,2,67000,189000
Kingston upon Hull, City of,E06000010,3,84000,259000
Kingston upon Hull, City of,E06000010,4,110000,415000
"Kingston upon Hull, City of",E06000010,1,63000,189000
"Kingston upon Hull, City of",E06000010,2,67000,189000
"Kingston upon Hull, City of",E06000010,3,84000,259000
"Kingston upon Hull, City of",E06000010,4,110000,415000
Kingston upon Thames,E09000021,1,156000,649000
Kingston upon Thames,E09000021,2,325000,708000
Kingston upon Thames,E09000021,3,398000,935000

Can't render this file because it has a wrong number of fields in line 90.

8
db/migrate/20230215112932_add_old_id_to_sales_logs.rb

@ -0,0 +1,8 @@
class AddOldIdToSalesLogs < ActiveRecord::Migration[7.0]
def change
change_table :sales_logs, bulk: true do |t|
t.column :old_id, :string
end
add_index :sales_logs, :old_id, unique: true
end
end

5
db/migrate/20230301120116_add_category_to_bulk_upload_errors.rb

@ -0,0 +1,5 @@
class AddCategoryToBulkUploadErrors < ActiveRecord::Migration[7.0]
def change
add_column :bulk_upload_errors, :category, :text, null: true
end
end

5
db/migrate/20230301144555_add_pregblank.rb

@ -0,0 +1,5 @@
class AddPregblank < ActiveRecord::Migration[7.0]
def change
add_column :sales_logs, :pregblank, :integer
end
end

13
db/migrate/20230308101826_create_local_authorities.rb

@ -0,0 +1,13 @@
class CreateLocalAuthorities < ActiveRecord::Migration[7.0]
def change
create_table :local_authorities do |t|
t.string :code, null: false
t.string :name, null: false
t.datetime :start_date, null: false
t.datetime :end_date
t.index %w[code], name: "index_local_authority_code", unique: true
t.timestamps
end
end
end

24
db/schema.rb

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_02_13_140932) do
ActiveRecord::Schema[7.0].define(version: 2023_03_08_101826) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -26,6 +26,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_13_140932) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "col"
t.text "category"
t.index ["bulk_upload_id"], name: "index_bulk_upload_errors_on_bulk_upload_id"
end
@ -288,6 +289,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_13_140932) do
t.index ["updated_by_id"], name: "index_lettings_logs_on_updated_by_id"
end
create_table "local_authorities", force: :cascade do |t|
t.string "code", null: false
t.string "name", null: false
t.datetime "start_date", null: false
t.datetime "end_date"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["code"], name: "index_local_authority_code", unique: true
end
create_table "location_deactivation_periods", force: :cascade do |t|
t.datetime "deactivation_date"
t.datetime "reactivation_date"
@ -490,7 +501,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_13_140932) do
t.integer "prevten"
t.integer "mortgageused"
t.integer "wchair"
t.integer "income2_value_check"
t.integer "armedforcesspouse"
t.datetime "hodate", precision: nil
t.integer "hoday"
@ -515,13 +525,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_13_140932) do
t.integer "retirement_value_check"
t.integer "hodate_check"
t.integer "extrabor_value_check"
t.integer "grant_value_check"
t.integer "staircase_bought_value_check"
t.integer "deposit_and_mortgage_value_check"
t.integer "shared_ownership_deposit_value_check"
t.integer "grant_value_check"
t.integer "value_value_check"
t.integer "old_persons_shared_ownership_value_check"
t.integer "staircase_bought_value_check"
t.integer "income2_value_check"
t.integer "monthly_charges_value_check"
t.integer "value_value_check"
t.integer "details_known_5"
t.integer "details_known_6"
t.integer "saledate_check"
@ -530,8 +541,11 @@ ActiveRecord::Schema[7.0].define(version: 2023_02_13_140932) do
t.integer "ethnic_group2"
t.integer "ethnicbuy2"
t.integer "proplen_asked"
t.string "old_id"
t.integer "pregblank"
t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id"
t.index ["old_id"], name: "index_sales_logs_on_old_id", unique: true
t.index ["owning_organisation_id"], name: "index_sales_logs_on_owning_organisation_id"
t.index ["updated_by_id"], name: "index_sales_logs_on_updated_by_id"
end

7
db/seeds.rb

@ -324,6 +324,11 @@ unless Rails.env.test?
service.call
end
end
puts LaSaleRange.count
end
if LocalAuthority.count.zero?
path = "config/local_authorities_data/initial_local_authorities.csv"
service = Imports::LocalAuthoritiesService.new(path:)
service.call
end
# rubocop:enable Rails/Output

2
lib/tasks/data_import.rake

@ -22,6 +22,8 @@ namespace :core do
Imports::OrganisationRentPeriodImportService.new(storage_service).create_organisation_rent_periods(path)
when "lettings-logs"
Imports::LettingsLogsImportService.new(storage_service).create_logs(path)
when "sales-logs"
Imports::SalesLogsImportService.new(storage_service).create_logs(path)
else
raise "Type #{type} is not supported by data_import"
end

1
lib/tasks/full_import.rake

@ -18,6 +18,7 @@ namespace :core do
Import.new(Imports::DataProtectionConfirmationImportService, :create_data_protection_confirmations, "dataprotect"),
Import.new(Imports::OrganisationRentPeriodImportService, :create_organisation_rent_periods, "rent-period"),
Import.new(Imports::LettingsLogsImportService, :create_logs, "logs"),
# Import.new(Imports::SalesLogsImportService, :create_logs, "logs"),
]
import_list.each do |step|

13
lib/tasks/local_authorities.rake

@ -0,0 +1,13 @@
namespace :data_import do
desc "Import local authorities data"
task :local_authorities, %i[path] => :environment do |_task, args|
path = args[:path]
raise "Usage: rake data_import:local_authorities['path/to/csv_file']" if path.blank?
service = Imports::LocalAuthoritiesService.new(path:)
service.call
pp "Created/updated #{service.count} local authority records" unless Rails.env.test?
end
end

BIN
public/files/bulk-upload-lettings-specification-2022-23.xlsx

Binary file not shown.

BIN
public/files/bulk-upload-sales-specification-2022-23.xlsx

Binary file not shown.

35
spec/components/bulk_upload_error_summary_table_component_spec.rb

@ -5,6 +5,10 @@ RSpec.describe BulkUploadErrorSummaryTableComponent, type: :component do
let(:bulk_upload) { create(:bulk_upload) }
before do
stub_const("BulkUploadErrorSummaryTableComponent::DISPLAY_THRESHOLD", 0)
end
context "when no errors" do
it "does not renders any rows" do
result = render_inline(component)
@ -12,6 +16,19 @@ RSpec.describe BulkUploadErrorSummaryTableComponent, type: :component do
end
end
context "when below threshold" do
before do
stub_const("BulkUploadErrorSummaryTableComponent::DISPLAY_THRESHOLD", 16)
create(:bulk_upload_error, bulk_upload:, col: "A", row: 1)
end
it "does not render rows" do
result = render_inline(component)
expect(result).to have_selector("tbody tr", count: 0)
end
end
context "when there are 2 independent errors" do
let!(:error_2) { create(:bulk_upload_error, bulk_upload:, col: "B", row: 2) }
let!(:error_1) { create(:bulk_upload_error, bulk_upload:, col: "A", row: 1) }
@ -78,4 +95,22 @@ RSpec.describe BulkUploadErrorSummaryTableComponent, type: :component do
])
end
end
describe "#errors?" do
context "when there are no errors" do
it "returns false" do
expect(component).not_to be_errors
end
end
context "when there are errors" do
before do
create(:bulk_upload_error, bulk_upload:, col: "A", row: 2, field: "field_1")
end
it "returns true" do
expect(component).to be_errors
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save