Browse Source

Merge branch 'main' into CLDC-1864-single-log-numbering

# Conflicts:
#	app/models/form/lettings/questions/age.rb
pull/1381/head
natdeanlewissoftwire 3 years ago
parent
commit
706f8094ce
  1. 1
      Gemfile
  2. 3
      Gemfile.lock
  3. 2
      app/components/search_component.rb
  4. 5
      app/controllers/bulk_upload_lettings_logs_controller.rb
  5. 5
      app/controllers/bulk_upload_sales_logs_controller.rb
  6. 16
      app/helpers/logs_helper.rb
  7. 39
      app/mailers/bulk_upload_mailer.rb
  8. 29
      app/models/derived_variables/lettings_log_variables.rb
  9. 14
      app/models/form/lettings/pages/person_age.rb
  10. 7
      app/models/form/lettings/questions/age.rb
  11. 2
      app/models/form/lettings/questions/housingneeds_other.rb
  12. 2
      app/models/form/lettings/questions/net_income_known.rb
  13. 4
      app/models/form/lettings/questions/offered_social_let.rb
  14. 2
      app/models/form/lettings/questions/person_relationship.rb
  15. 3
      app/models/form/lettings/questions/rsnvac.rb
  16. 7
      app/models/form/lettings/subsections/household_characteristics.rb
  17. 9
      app/models/form/sales/pages/about_staircase.rb
  18. 9
      app/models/form/sales/pages/buyer_previous.rb
  19. 9
      app/models/form/sales/pages/previous_ownership.rb
  20. 8
      app/models/form/sales/questions/buyer_previous.rb
  21. 8
      app/models/form/sales/questions/prevown.rb
  22. 2
      app/models/form/sales/questions/property_local_authority_known.rb
  23. 8
      app/models/form/sales/questions/staircase_owned.rb
  24. 6
      app/models/form/sales/subsections/household_characteristics.rb
  25. 3
      app/models/form/sales/subsections/income_benefits_and_savings.rb
  26. 6
      app/models/form/sales/subsections/shared_ownership_scheme.rb
  27. 4
      app/models/forms/bulk_upload_lettings/prepare_your_file.rb
  28. 4
      app/models/forms/bulk_upload_sales/prepare_your_file.rb
  29. 1
      app/models/lettings_log.rb
  30. 2
      app/models/local_authority.rb
  31. 7
      app/models/log.rb
  32. 11
      app/models/sales_log.rb
  33. 4
      app/models/validations/sales/household_validations.rb
  34. 75
      app/services/bulk_upload/lettings/row_parser.rb
  35. 7
      app/services/bulk_upload/lettings/validator.rb
  36. 32
      app/services/bulk_upload/processor.rb
  37. 25
      app/services/imports/local_authorities_service.rb
  38. 66
      app/services/imports/sales_logs_import_service.rb
  39. 2
      app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb
  40. 2
      app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb
  41. 52
      app/views/bulk_upload_shared/guidance.html.erb
  42. 4
      app/views/logs/index.html.erb
  43. 388
      config/local_authorities_data/initial_local_authorities.csv
  44. 20
      config/locales/en.yml
  45. 2
      config/routes.rb
  46. 5
      db/migrate/20230301120116_add_category_to_bulk_upload_errors.rb
  47. 13
      db/migrate/20230308101826_create_local_authorities.rb
  48. 13
      db/schema.rb
  49. 6
      db/seeds.rb
  50. 13
      lib/tasks/local_authorities.rake
  51. BIN
      public/files/bulk-upload-lettings-specification-2022-23.xlsx
  52. BIN
      public/files/bulk-upload-sales-specification-2022-23.xlsx
  53. 1
      spec/factories/sales_log.rb
  54. 6
      spec/fixtures/files/local_authorities.csv
  55. 2
      spec/fixtures/imports/sales_logs/shared_ownership_sales_log3.xml
  56. 43
      spec/lib/tasks/local_authorities_import_spec.rb
  57. 32
      spec/mailers/bulk_upload_mailer_spec.rb
  58. 27
      spec/models/form/lettings/pages/person_age_spec.rb
  59. 17
      spec/models/form/lettings/questions/age_spec.rb
  60. 17
      spec/models/form/lettings/questions/net_income_known_spec.rb
  61. 44
      spec/models/form/lettings/questions/offered_social_let_spec.rb
  62. 2
      spec/models/form/lettings/questions/person_relationship_spec.rb
  63. 79
      spec/models/form/lettings/questions/rsnvac_spec.rb
  64. 21
      spec/models/form/lettings/subsections/household_characteristics_spec.rb
  65. 21
      spec/models/form/sales/pages/about_staircase_spec.rb
  66. 21
      spec/models/form/sales/pages/buyer_previous_spec.rb
  67. 19
      spec/models/form/sales/pages/previous_ownership_spec.rb
  68. 17
      spec/models/form/sales/questions/buyer_previous_spec.rb
  69. 17
      spec/models/form/sales/questions/prevown_spec.rb
  70. 17
      spec/models/form/sales/questions/staircase_owned_spec.rb
  71. 18
      spec/models/form/sales/subsections/household_characteristics_spec.rb
  72. 6
      spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb
  73. 6
      spec/models/form/sales/subsections/shared_ownership_scheme_spec.rb
  74. 159
      spec/models/lettings_log_spec.rb
  75. 24
      spec/requests/bulk_upload_lettings_logs_controller_spec.rb
  76. 24
      spec/requests/bulk_upload_sales_logs_controller_spec.rb
  77. 164
      spec/services/bulk_upload/lettings/row_parser_spec.rb
  78. 20
      spec/services/bulk_upload/lettings/validator_spec.rb
  79. 39
      spec/services/bulk_upload/processor_spec.rb
  80. 54
      spec/services/filter_service_spec.rb
  81. 318
      spec/services/imports/sales_logs_import_service_spec.rb
  82. 16
      spec/support/bulk_upload/log_to_csv.rb

1
Gemfile

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

3
Gemfile.lock

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

2
app/components/search_component.rb

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

5
app/controllers/bulk_upload_lettings_logs_controller.rb

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

5
app/controllers/bulk_upload_sales_logs_controller.rb

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

16
app/helpers/logs_helper.rb

@ -23,4 +23,20 @@ module LogsHelper
array = bulk_upload ? [bulk_upload.id] : [] array = bulk_upload ? [bulk_upload.id] : []
array.index_with { |_bulk_upload_id| "With logs from bulk upload" } array.index_with { |_bulk_upload_id| "With logs from bulk upload" }
end 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 end

39
app/mailers/bulk_upload_mailer.rb

@ -66,17 +66,40 @@ class BulkUploadMailer < NotifyMailer
) )
end 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( send_email(
user.email, bulk_upload.user.email,
BULK_UPLOAD_FAILED_FILE_SETUP_ERROR_TEMPLATE_ID, BULK_UPLOAD_FAILED_FILE_SETUP_ERROR_TEMPLATE_ID,
{ {
filename: "[#{bulk_upload} filename]", filename: bulk_upload.filename,
upload_timestamp: "[#{bulk_upload} upload_timestamp]", upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time),
lettings_or_sales: "[#{bulk_upload} lettings_or_sales]", lettings_or_sales: bulk_upload.log_type,
year_combo: "[#{bulk_upload} year_combo]", year_combo: bulk_upload.year_combo,
errors_list: "[#{bulk_upload} errors_list]", errors_list: errors.join("\n"),
bulk_upload_link: "[#{bulk_upload} bulk_upload_link]", bulk_upload_link:,
}, },
) )
end end

29
app/models/derived_variables/lettings_log_variables.rb

@ -1,5 +1,31 @@
module DerivedVariables::LettingsLogVariables 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? def scheme_has_multiple_locations?
return false unless scheme return false unless scheme
@ -49,6 +75,7 @@ module DerivedVariables::LettingsLogVariables
self.voiddate = startdate self.voiddate = startdate
self.first_time_property_let_as_social_housing = 0 self.first_time_property_let_as_social_housing = 0
self.rsnvac = 14 self.rsnvac = 14
self.unitletas = form.start_date.year >= 2023 ? UNITLETAS_MAPPING_23_24[rent_type] : UNITLETAS_MAPPING[rent_type]
if is_general_needs? if is_general_needs?
# fixed term # fixed term
self.prevten = 32 if managing_organisation&.provider_type == "PRP" self.prevten = 32 if managing_organisation&.provider_type == "PRP"

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

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

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

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

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

@ -3,7 +3,7 @@ class Form::Lettings::Questions::HousingneedsOther < ::Form::Question
super super
@id = "housingneeds_other" @id = "housingneeds_other"
@check_answer_label = "Other disabled access needs" @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" @type = "radio"
@check_answers_card_number = 0 @check_answers_card_number = 0
@hint_text = "" @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) def initialize(id, hsh, page)
super super
@id = "net_income_known" @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?" @header = "Do you know the household’s combined income after tax?"
@type = "radio" @type = "radio"
@check_answers_card_number = 0 @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 super
@id = "offered" @id = "offered"
@check_answer_label = "Times previously offered since becoming available" @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" @type = "numeric"
@width = 2 @width = 2
@check_answers_card_number = 0 @check_answers_card_number = 0
@max = 150 @max = 150
@min = 0 @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 @step = 1
@question_number = 18 @question_number = 18
end end

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

@ -15,7 +15,7 @@ class Form::Lettings::Questions::PersonRelationship < ::Form::Question
"P" => { "value" => "Partner" }, "P" => { "value" => "Partner" },
"C" => { "C" => {
"value" => "Child", "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" }, "X" => { "value" => "Other" },
"divider" => { "value" => true }, "divider" => { "value" => true },

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

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

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::LeadTenantOverRetirementValueCheck.new(nil, nil, self),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 2), 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::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::PersonAge.new(nil, nil, self, person_index: 2),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 2), 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::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 2),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 3), 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::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::PersonAge.new(nil, nil, self, person_index: 3),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 3), 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::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 3),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 4), 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::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::PersonAge.new(nil, nil, self, person_index: 4),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 4), 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::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 4),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 5), 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::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::PersonAge.new(nil, nil, self, person_index: 5),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 5), 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::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 5),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 6), 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::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::PersonAge.new(nil, nil, self, person_index: 6),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 6), 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::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 6),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 7), 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::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::PersonAge.new(nil, nil, self, person_index: 7),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 7), 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::PersonOverRetirementValueCheck.new(nil, nil, self, person_index: 7),
Form::Lettings::Pages::PersonKnown.new(nil, nil, self, person_index: 8), 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::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::PersonAge.new(nil, nil, self, person_index: 8),
Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self, Form::Lettings::Pages::NoFemalesPregnantHouseholdPersonAgeValueCheck.new(nil, nil, self,
person_index: 8), person_index: 8),

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

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

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

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

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

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

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

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

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

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

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

@ -3,7 +3,7 @@ class Form::Sales::Questions::PropertyLocalAuthorityKnown < ::Form::Question
super super
@id = "la_known" @id = "la_known"
@check_answer_label = "Local authority 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" @type = "radio"
@answer_options = ANSWER_OPTIONS @answer_options = ANSWER_OPTIONS
@conditional_for = { "la" => [1] } @conditional_for = { "la" => [1] }

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

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

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

@ -3,7 +3,7 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
super super
@id = "household_characteristics" @id = "household_characteristics"
@label = "Household characteristics" @label = "Household characteristics"
@depends_on = [{ "setup_completed?" => true }] @depends_on = [{ "setup_completed?" => true, "company_buyer?" => false }]
end end
def pages def pages
@ -92,4 +92,8 @@ class Form::Sales::Subsections::HouseholdCharacteristics < ::Form::Subsection
Form::Sales::Pages::Buyer2EthnicBackgroundWhite.new(nil, nil, self)] Form::Sales::Pages::Buyer2EthnicBackgroundWhite.new(nil, nil, self)]
end end
end end
def displayed_in_tasklist?(log)
!log.company_buyer?
end
end end

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

@ -23,7 +23,8 @@ class Form::Sales::Subsections::IncomeBenefitsAndSavings < ::Form::Subsection
Form::Sales::Pages::Savings.new(nil, nil, self), Form::Sales::Pages::Savings.new(nil, nil, self),
Form::Sales::Pages::SavingsValueCheck.new("savings_value_check", 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::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, previous_shared_page,
].compact ].compact
end end

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

@ -10,14 +10,16 @@ class Form::Sales::Subsections::SharedOwnershipScheme < ::Form::Subsection
@pages ||= [ @pages ||= [
Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership", nil, self, ownershipsch: 1), Form::Sales::Pages::LivingBeforePurchase.new("living_before_purchase_shared_ownership", nil, self, ownershipsch: 1),
Form::Sales::Pages::Staircase.new(nil, 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::StaircaseBoughtValueCheck.new(nil, nil, self),
Form::Sales::Pages::Resale.new(nil, nil, self), Form::Sales::Pages::Resale.new(nil, nil, self),
Form::Sales::Pages::ExchangeDate.new(nil, nil, self), Form::Sales::Pages::ExchangeDate.new(nil, nil, self),
Form::Sales::Pages::HandoverDate.new(nil, nil, self), Form::Sales::Pages::HandoverDate.new(nil, nil, self),
Form::Sales::Pages::HandoverDateCheck.new(nil, nil, self), Form::Sales::Pages::HandoverDateCheck.new(nil, nil, self),
Form::Sales::Pages::LaNominations.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::PreviousBedrooms.new(nil, nil, self),
Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self), Form::Sales::Pages::PreviousPropertyType.new(nil, nil, self),
Form::Sales::Pages::PreviousTenure.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" "/files/bulk-upload-lettings-template-v1.xlsx"
end end
def specification_path
"/files/bulk-upload-lettings-specification-2022-23.xlsx"
end
def year_combo def year_combo
"#{year}/#{year + 1 - 2000}" "#{year}/#{year + 1 - 2000}"
end 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" "/files/bulk-upload-sales-template-v1.xlsx"
end end
def specification_path
"/files/bulk-upload-sales-specification-2022-23.xlsx"
end
def year_combo def year_combo
"#{year}/#{year + 1 - 2000}" "#{year}/#{year + 1 - 2000}"
end end

1
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_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_tenant_code, ->(tenant_code) { where("tenancycode ILIKE ?", "%#{tenant_code}%") }
scope :filter_by_propcode, ->(propcode) { where("propcode ILIKE ?", "%#{propcode}%") } 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 :filter_by_location_postcode, ->(postcode_full) { left_joins(:location).where("REPLACE(locations.postcode, ' ', '') ILIKE ?", "%#{postcode_full.delete(' ')}%") }
scope :search_by, lambda { |param| scope :search_by, lambda { |param|
filter_by_location_postcode(param) filter_by_location_postcode(param)

2
app/models/local_authority.rb

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

7
app/models/log.rb

@ -18,6 +18,7 @@ class Log < ApplicationRecord
years.each { |year| query = query.or(filter_by_year(year)) } years.each { |year| query = query.or(filter_by_year(year)) }
query.all 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_id, ->(id) { where(id:) }
scope :filter_by_user, lambda { |selected_user, user| scope :filter_by_user, lambda { |selected_user, user|
if !selected_user.include?("all") && user.present? if !selected_user.include?("all") && user.present?
@ -80,6 +81,12 @@ class Log < ApplicationRecord
end end
end end
(2..8).each do |person_num|
define_method("person_#{person_num}_child_relation?") do
send("relat#{person_num}") == "C"
end
end
private private
def plural_gender_for_person(person_num) def plural_gender_for_person(person_num)

11
app/models/sales_log.rb

@ -33,7 +33,12 @@ class SalesLog < Log
before_validation :set_derived_fields! 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 :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) } 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 OPTIONAL_FIELDS = %w[saledate_check purchid monthly_charges_value_check old_persons_shared_ownership_value_check].freeze
@ -257,6 +262,10 @@ class SalesLog < Log
ownershipsch == 1 ownershipsch == 1
end end
def company_buyer?
companybuy == 1
end
def buyers_age_for_old_persons_shared_ownership_invalid? def buyers_age_for_old_persons_shared_ownership_invalid?
return unless old_persons_shared_ownership? return unless old_persons_shared_ownership?

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? return unless record.postcode_full && record.ppostcode_full && record.discounted_ownership_sale?
unless record.postcode_full == record.ppostcode_full unless record.postcode_full == record.ppostcode_full
record.errors.add :postcode_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, 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
end end

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

@ -143,6 +143,20 @@ class BulkUpload::Lettings::RowParser
validates :field_1, presence: { message: I18n.t("validations.not_answered", question: "letting type") }, 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") } 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_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") } validates :field_98, format: { with: /\A\d{2}\z/, message: I18n.t("validations.setup.startdate.year_not_two_digits") }
validate :validate_data_types validate :validate_data_types
@ -181,7 +195,12 @@ class BulkUpload::Lettings::RowParser
log.errors.each do |error| log.errors.each do |error|
fields = field_mapping_for_errors[error.attribute] || [] 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 end
errors.blank? errors.blank?
@ -203,6 +222,10 @@ class BulkUpload::Lettings::RowParser
block_log_creation block_log_creation
end end
def setup_section_incomplete?
log.form.setup_sections[0].subsections[0].is_incomplete?(log)
end
private private
def validate_location_related def validate_location_related
@ -222,7 +245,7 @@ private
def validate_location_exists def validate_location_exists
if scheme && field_5.present? && location.nil? if scheme && field_5.present? && location.nil?
errors.add(:field_5, "Location could be found with provided scheme code") errors.add(:field_5, "Location could be found with provided scheme code", category: :setup)
end end
end end
@ -240,7 +263,7 @@ private
def validate_scheme_exists def validate_scheme_exists
if field_4.present? && scheme.nil? if field_4.present? && scheme.nil?
errors.add(:field_4, "The management group code is not correct") errors.add(:field_4, "The management group code is not correct", category: :setup)
end end
end end
@ -254,7 +277,7 @@ private
def validate_managing_org_exists def validate_managing_org_exists
if managing_organisation.nil? if managing_organisation.nil?
errors.delete(:field_113) errors.delete(:field_113)
errors.add(:field_113, "The managing organisation code is incorrect") errors.add(:field_113, "The managing organisation code is incorrect", category: :setup)
end end
end end
@ -269,7 +292,7 @@ private
def validate_owning_org_exists def validate_owning_org_exists
if owning_organisation.nil? if owning_organisation.nil?
errors.delete(:field_111) errors.delete(:field_111)
errors.add(:field_111, "The owning organisation code is incorrect") errors.add(:field_111, "The owning organisation code is incorrect", category: :setup)
end end
end end
@ -337,7 +360,7 @@ private
end end
def validate_relevant_collection_window 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) unless bulk_upload.form.valid_start_date_for_form?(start_date)
errors.add(:field_96, I18n.t("validations.date.outside_collection_window")) errors.add(:field_96, I18n.t("validations.date.outside_collection_window"))
@ -347,6 +370,8 @@ private
end end
def start_date 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}") Date.parse("20#{field_98.to_s.rjust(2, '0')}-#{field_97}-#{field_96}")
rescue StandardError rescue StandardError
nil nil
@ -387,9 +412,25 @@ private
next if log.optional_fields.include?(question.id) next if log.optional_fields.include?(question.id)
next if question.completed?(log) 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
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 def field_mapping_for_errors
{ {
@ -398,6 +439,8 @@ private
postcode_known: %i[field_107 field_108 field_109], postcode_known: %i[field_107 field_108 field_109],
postcode_full: %i[field_107 field_108 field_109], postcode_full: %i[field_107 field_108 field_109],
la: %i[field_107], la: %i[field_107],
owning_organisation: [:field_111],
managing_organisation: [:field_113],
owning_organisation_id: [:field_111], owning_organisation_id: [:field_111],
managing_organisation_id: [:field_113], managing_organisation_id: [:field_113],
renewal: [:field_134], renewal: [:field_134],
@ -628,28 +671,28 @@ private
attributes["declaration"] = field_132 attributes["declaration"] = field_132
attributes["age1_known"] = field_12 == "R" ? 1 : 0 attributes["age1_known"] = field_12 == "R" ? 1 : 0
attributes["age1"] = field_12 if attributes["age1_known"].zero? attributes["age1"] = field_12 if attributes["age1_known"].zero? && field_12&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age2_known"] = field_13 == "R" ? 1 : 0 attributes["age2_known"] = field_13 == "R" ? 1 : 0
attributes["age2"] = field_13 if attributes["age2_known"].zero? attributes["age2"] = field_13 if attributes["age2_known"].zero? && field_13&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age3_known"] = field_14 == "R" ? 1 : 0 attributes["age3_known"] = field_14 == "R" ? 1 : 0
attributes["age3"] = field_14 if attributes["age3_known"].zero? attributes["age3"] = field_14 if attributes["age3_known"].zero? && field_14&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age4_known"] = field_15 == "R" ? 1 : 0 attributes["age4_known"] = field_15 == "R" ? 1 : 0
attributes["age4"] = field_15 if attributes["age4_known"].zero? attributes["age4"] = field_15 if attributes["age4_known"].zero? && field_15&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age5_known"] = field_16 == "R" ? 1 : 0 attributes["age5_known"] = field_16 == "R" ? 1 : 0
attributes["age5"] = field_16 if attributes["age5_known"].zero? attributes["age5"] = field_16 if attributes["age5_known"].zero? && field_16&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age6_known"] = field_17 == "R" ? 1 : 0 attributes["age6_known"] = field_17 == "R" ? 1 : 0
attributes["age6"] = field_17 if attributes["age6_known"].zero? attributes["age6"] = field_17 if attributes["age6_known"].zero? && field_17&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age7_known"] = field_18 == "R" ? 1 : 0 attributes["age7_known"] = field_18 == "R" ? 1 : 0
attributes["age7"] = field_18 if attributes["age7_known"].zero? attributes["age7"] = field_18 if attributes["age7_known"].zero? && field_18&.match(/\A\d{1,3}\z|\AR\z/)
attributes["age8_known"] = field_19 == "R" ? 1 : 0 attributes["age8_known"] = field_19 == "R" ? 1 : 0
attributes["age8"] = field_19 if attributes["age8_known"].zero? attributes["age8"] = field_19 if attributes["age8_known"].zero? && field_19&.match(/\A\d{1,3}\z|\AR\z/)
attributes["sex1"] = field_20 attributes["sex1"] = field_20
attributes["sex2"] = field_21 attributes["sex2"] = field_21
@ -868,6 +911,8 @@ private
0 0
when nil when nil
rsnvac == 14 ? 1 : 0 rsnvac == 14 ? 1 : 0
else
field_134
end end
end end

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

@ -168,6 +168,7 @@ class BulkUpload::Lettings::Validator
row:, row:,
cell: "#{cols[field_number_for_attribute(error.attribute) - col_offset + 1]}#{row}", cell: "#{cols[field_number_for_attribute(error.attribute) - col_offset + 1]}#{row}",
col: cols[field_number_for_attribute(error.attribute) - col_offset + 1], col: cols[field_number_for_attribute(error.attribute) - col_offset + 1],
category: error.options[:category],
) )
end end
end end
@ -185,12 +186,12 @@ class BulkUpload::Lettings::Validator
QUESTIONS[field] QUESTIONS[field]
end end
private
def any_setup_sections_incomplete? def any_setup_sections_incomplete?
row_parsers.any? { |row_parser| row_parser.log.form.setup_sections[0].subsections[0].is_incomplete?(row_parser.log) } row_parsers.any?(&:setup_section_incomplete?)
end end
private
def over_column_error_threshold? def over_column_error_threshold?
fields = ("field_1".."field_134").to_a fields = ("field_1".."field_134").to_a
percentage_threshold = (row_parsers.size * COLUMN_PERCENTAGE_ERROR_THRESHOLD).ceil percentage_threshold = (row_parsers.size * COLUMN_PERCENTAGE_ERROR_THRESHOLD).ceil

32
app/services/bulk_upload/processor.rb

@ -12,11 +12,15 @@ class BulkUpload::Processor
validator.call validator.call
create_logs if validator.create_logs? if validator.any_setup_sections_incomplete?
send_correct_and_upload_again_mail unless validator.create_logs? send_setup_errors_mail
elsif validator.create_logs?
create_logs
send_fix_errors_mail if created_logs_but_incompleted? send_fix_errors_mail if created_logs_but_incompleted?
send_success_mail if created_logs_and_all_completed? send_success_mail if created_logs_and_all_completed?
else
send_correct_and_upload_again_mail
end
rescue StandardError => e rescue StandardError => e
Sentry.capture_exception(e) Sentry.capture_exception(e)
send_failure_mail send_failure_mail
@ -26,16 +30,28 @@ class BulkUpload::Processor
private 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 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 end
def send_fix_errors_mail 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 end
def send_success_mail 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 end
def created_logs_but_incompleted? def created_logs_but_incompleted?
@ -47,7 +63,9 @@ private
end end
def send_failure_mail 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 end
def user def user

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

66
app/services/imports/sales_logs_import_service.rb

@ -16,7 +16,9 @@ module Imports
private private
def create_log(xml_doc) 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 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 = {} attributes = {}
@ -76,7 +78,7 @@ module Imports
attributes["inc2mort"] = unsafe_string_as_integer(xml_doc, "Q2Person2MortApplication") attributes["inc2mort"] = unsafe_string_as_integer(xml_doc, "Q2Person2MortApplication")
attributes["hb"] = unsafe_string_as_integer(xml_doc, "Q2a") attributes["hb"] = unsafe_string_as_integer(xml_doc, "Q2a")
attributes["frombeds"] = safe_string_as_integer(xml_doc, "Q20Bedrooms") attributes["frombeds"] = safe_string_as_integer(xml_doc, "Q20Bedrooms")
attributes["staircase"] = unsafe_string_as_integer(xml_doc, "Q17aStaircase") attributes["staircase"] = unsafe_string_as_integer(xml_doc, "Q17aStaircase") if attributes["ownershipsch"] == 1
attributes["stairbought"] = safe_string_as_integer(xml_doc, "PercentBought") attributes["stairbought"] = safe_string_as_integer(xml_doc, "PercentBought")
attributes["stairowned"] = safe_string_as_integer(xml_doc, "PercentOwns") if attributes["staircase"] == 1 attributes["stairowned"] = safe_string_as_integer(xml_doc, "PercentOwns") if attributes["staircase"] == 1
attributes["mrent"] = safe_string_as_decimal(xml_doc, "Q28MonthlyRent") attributes["mrent"] = safe_string_as_decimal(xml_doc, "Q28MonthlyRent")
@ -102,7 +104,6 @@ module Imports
attributes["ppcodenk"] = previous_postcode_known(xml_doc, attributes["ppostcode_full"], attributes["prevloc"]) # Q7UNKNOWNPOSTCODE check mapping attributes["ppcodenk"] = previous_postcode_known(xml_doc, attributes["ppostcode_full"], attributes["prevloc"]) # Q7UNKNOWNPOSTCODE check mapping
attributes["ppostc1"] = string_or_nil(xml_doc, "PPOSTC1") attributes["ppostc1"] = string_or_nil(xml_doc, "PPOSTC1")
attributes["ppostc2"] = string_or_nil(xml_doc, "PPOSTC2") attributes["ppostc2"] = string_or_nil(xml_doc, "PPOSTC2")
attributes["previous_la_known"] = nil
attributes["hhregres"] = unsafe_string_as_integer(xml_doc, "ArmedF") attributes["hhregres"] = unsafe_string_as_integer(xml_doc, "ArmedF")
attributes["hhregresstill"] = still_serving(xml_doc) attributes["hhregresstill"] = still_serving(xml_doc)
attributes["proplen"] = safe_string_as_integer(xml_doc, "Q16aProplen2") || safe_string_as_integer(xml_doc, "Q16aProplensec2") attributes["proplen"] = safe_string_as_integer(xml_doc, "Q16aProplen2") || safe_string_as_integer(xml_doc, "Q16aProplensec2")
@ -111,7 +112,7 @@ module Imports
attributes["prevten"] = unsafe_string_as_integer(xml_doc, "Q6PrevTenure") attributes["prevten"] = unsafe_string_as_integer(xml_doc, "Q6PrevTenure")
attributes["mortlen"] = mortgage_length(xml_doc, attributes) attributes["mortlen"] = mortgage_length(xml_doc, attributes)
attributes["extrabor"] = borrowing(xml_doc, attributes) attributes["extrabor"] = borrowing(xml_doc, attributes)
attributes["mortgageused"] = unsafe_string_as_integer(xml_doc, "MORTGAGEUSED") attributes["mortgageused"] = mortgage_used(xml_doc, attributes)
attributes["wchair"] = unsafe_string_as_integer(xml_doc, "Q15Wheelchair") attributes["wchair"] = unsafe_string_as_integer(xml_doc, "Q15Wheelchair")
attributes["armedforcesspouse"] = unsafe_string_as_integer(xml_doc, "ARMEDFORCESSPOUSE") attributes["armedforcesspouse"] = unsafe_string_as_integer(xml_doc, "ARMEDFORCESSPOUSE")
attributes["hodate"] = compose_date(xml_doc, "HODAY", "HOMONTH", "HOYEAR") attributes["hodate"] = compose_date(xml_doc, "HODAY", "HOMONTH", "HOYEAR")
@ -130,13 +131,29 @@ module Imports
attributes["prevshared"] = nil # 23/24 variable attributes["prevshared"] = nil # 23/24 variable
attributes["staircasesale"] = nil # 23/24 variable attributes["staircasesale"] = nil # 23/24 variable
# Required for our form invalidated questions (not present in import) attributes["previous_la_known"] = 1 if attributes["prevloc"].present?
attributes["previous_la_known"] = 1 if attributes["prevloc"].present? && attributes["ppostcode_full"].blank? if attributes["la"].present?
if attributes["la"].present? && attributes["postcode_full"].blank?
attributes["la_known"] = 1 attributes["la_known"] = 1
attributes["is_la_inferred"] = false attributes["is_la_inferred"] = false
end 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 # Sets the log creator
owner_id = meta_field_value(xml_doc, "owner-user-id").strip owner_id = meta_field_value(xml_doc, "owner-user-id").strip
if owner_id.present? if owner_id.present?
@ -168,10 +185,32 @@ module Imports
end end
end end
def rescue_validation_or_raise(sales_log, _attributes, _previous_status, exception) 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") @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 raise exception
end end
end
def compute_differences(sales_log, attributes) def compute_differences(sales_log, attributes)
differences = [] differences = []
@ -189,6 +228,7 @@ module Imports
def fields_not_present_in_softwire_data def fields_not_present_in_softwire_data
%w[created_by %w[created_by
income1_value_check income1_value_check
income2_value_check
mortgage_value_check mortgage_value_check
savings_value_check savings_value_check
deposit_value_check deposit_value_check
@ -388,7 +428,6 @@ module Imports
end end
def monthly_charges(xml_doc, attributes) def monthly_charges(xml_doc, attributes)
safe_string_as_decimal(xml_doc, "Q29MonthlyCharges")
case attributes["ownershipsch"] case attributes["ownershipsch"]
when 1 when 1
safe_string_as_decimal(xml_doc, "Q29MonthlyCharges") safe_string_as_decimal(xml_doc, "Q29MonthlyCharges")
@ -446,6 +485,17 @@ module Imports
UKPostcode.parse(postcode).to_s UKPostcode.parse(postcode).to_s
end 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) def set_default_values(attributes)
attributes["armedforcesspouse"] ||= 7 attributes["armedforcesspouse"] ||= 7
attributes["hhregres"] ||= 8 attributes["hhregres"] ||= 8

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> <h2 class="govuk-heading-m">Create your file</h2>
<ul class="govuk-list govuk-list--bullet"> <ul class="govuk-list govuk-list--bullet">
<li>Download the <%= govuk_link_to "bulk lettings template", @form.template_path %></li> <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>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> <li>You can not have a file with both general needs logs and supported housing logs. These must be in separate files</li>
</ul> </ul>

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> <h2 class="govuk-heading-m">Create your file</h2>
<ul class="govuk-list govuk-list--bullet"> <ul class="govuk-list govuk-list--bullet">
<li>Download the <%= govuk_link_to "bulk sales template", @form.template_path %></li> <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> <li>If you cannot export it in this format, you may have to input it manually</li>
</ul> </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>

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

@ -64,9 +64,9 @@
<%= render partial: "log_filters" %> <%= render partial: "log_filters" %>
<div class="app-filter-layout__content"> <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") %> <%= 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" } %> <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "logs" } %>
</div> </div>
</div> </div>

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

@ -538,6 +538,15 @@ en:
W: "Suitable for someone who uses a wheelchair and offers the full use of all rooms and facilities." 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." 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." 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: hints:
location: location:
@ -551,6 +560,17 @@ en:
bulk_upload: 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." 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: warnings:
location: location:
deactivate: 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 resources :bulk_upload_lettings_logs, path: "bulk-upload-logs", only: %i[show update] do
collection do collection do
get :start get :start
get "guidance", to: "bulk_upload_lettings_logs#guidance"
end end
end end
@ -165,6 +166,7 @@ Rails.application.routes.draw do
resources :bulk_upload_sales_logs, path: "bulk-upload-logs" do resources :bulk_upload_sales_logs, path: "bulk-upload-logs" do
collection do collection do
get :start get :start
get "guidance", to: "bulk_upload_sales_logs#guidance"
end end
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

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

13
db/schema.rb

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_03_01_144555) do ActiveRecord::Schema[7.0].define(version: 2023_03_08_101826) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -26,6 +26,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_01_144555) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "col" t.text "col"
t.text "category"
t.index ["bulk_upload_id"], name: "index_bulk_upload_errors_on_bulk_upload_id" t.index ["bulk_upload_id"], name: "index_bulk_upload_errors_on_bulk_upload_id"
end end
@ -288,6 +289,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_01_144555) do
t.index ["updated_by_id"], name: "index_lettings_logs_on_updated_by_id" t.index ["updated_by_id"], name: "index_lettings_logs_on_updated_by_id"
end 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| create_table "location_deactivation_periods", force: :cascade do |t|
t.datetime "deactivation_date" t.datetime "deactivation_date"
t.datetime "reactivation_date" t.datetime "reactivation_date"

6
db/seeds.rb

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

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.

1
spec/factories/sales_log.rb

@ -14,7 +14,6 @@ FactoryBot.define do
ownershipsch { 2 } ownershipsch { 2 }
type { 8 } type { 8 }
saledate { Time.utc(2023, 2, 2, 10, 36, 49) } saledate { Time.utc(2023, 2, 2, 10, 36, 49) }
companybuy { 1 }
jointpur { 1 } jointpur { 1 }
beds { 2 } beds { 2 }
jointmore { 1 } jointmore { 1 }

6
spec/fixtures/files/local_authorities.csv vendored

@ -0,0 +1,6 @@
code,name,start_year,end_year
S12000033,Aberdeen City,2021,
S12000034,Aberdeenshire,2021,
E07000223,Adur,2021,
E07000032,Amber Valley,2021,
S12000041,Angus,2021,
1 code name start_year end_year
2 S12000033 Aberdeen City 2021
3 S12000034 Aberdeenshire 2021
4 E07000223 Adur 2021
5 E07000032 Amber Valley 2021
6 S12000041 Angus 2021

2
spec/fixtures/imports/sales_logs/shared_ownership_sales_log3.xml vendored

@ -144,7 +144,7 @@
<P7RRefused>0</P7RRefused> <P7RRefused>0</P7RRefused>
<P8RRefused>0</P8RRefused> <P8RRefused>0</P8RRefused>
<TotRRefused>0</TotRRefused> <TotRRefused>0</TotRRefused>
<CALCMORT>76000</CALCMORT> <CALCMORT></CALCMORT>
<MORTGAGEUSED>1</MORTGAGEUSED> <MORTGAGEUSED>1</MORTGAGEUSED>
<IM1>47000</IM1> <IM1>47000</IM1>
<IM2>0</IM2> <IM2>0</IM2>

43
spec/lib/tasks/local_authorities_import_spec.rb

@ -0,0 +1,43 @@
require "rails_helper"
require "rake"
RSpec.describe "data_import" do
describe ":local_authorities", type: :task do
subject(:task) { Rake::Task["data_import:local_authorities"] }
before do
LocalAuthority.destroy_all
Rake.application.rake_require("tasks/local_authorities")
Rake::Task.define_task(:environment)
task.reenable
end
context "when the rake task is run" do
let(:local_authorities_file_path) { "./spec/fixtures/files/local_authorities.csv" }
let(:wrong_file_path) { "/test/no_csv_here.csv" }
it "creates new local authorities records" do
expect { task.invoke(local_authorities_file_path) }.to change(LocalAuthority, :count).by(5)
expect(LocalAuthority.where(code: "S12000041").exists?).to be true
end
it "raises an error when no path is given" do
expect { task.invoke(nil) }.to raise_error(RuntimeError, "Usage: rake data_import:local_authorities['path/to/csv_file']")
end
it "raises an error when no file exists at the given path" do
expect { task.invoke(wrong_file_path) }.to raise_error(Errno::ENOENT)
end
context "when a record already exists with a matching code index" do
let!(:local_authority) { LocalAuthority.create(code: "S12000041", name: "Something else", start_date: Time.zone.local(2021, 4, 1)) }
it "updates local authority if the record is matched on code" do
task.invoke(local_authorities_file_path)
local_authority.reload
expect(local_authority.name).to eq("Angus")
end
end
end
end
end

32
spec/mailers/bulk_upload_mailer_spec.rb

@ -12,6 +12,38 @@ RSpec.describe BulkUploadMailer do
allow(notify_client).to receive(:send_email).and_return(true) allow(notify_client).to receive(:send_email).and_return(true)
end end
describe "#send_bulk_upload_failed_file_setup_error_mail" do
before do
create(:bulk_upload_error, bulk_upload:, col: "A", field: "field_1", category: "setup")
create(:bulk_upload_error, bulk_upload:, col: "E", field: "field_4", category: "setup")
create(:bulk_upload_error, bulk_upload:, col: "F", field: "field_5")
end
let(:expected_errors) do
[
"- Column A (What is the letting type?)",
"- Column E (Management group code)",
]
end
it "sends correctly formed email" do
expect(notify_client).to receive(:send_email).with(
email_address: bulk_upload.user.email,
template_id: described_class::BULK_UPLOAD_FAILED_FILE_SETUP_ERROR_TEMPLATE_ID,
personalisation: {
filename: bulk_upload.filename,
upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time),
lettings_or_sales: bulk_upload.log_type,
year_combo: bulk_upload.year_combo,
errors_list: expected_errors.join("\n"),
bulk_upload_link: start_bulk_upload_lettings_logs_url,
},
)
mailer.send_bulk_upload_failed_file_setup_error_mail(bulk_upload:)
end
end
describe "#send_bulk_upload_complete_mail" do describe "#send_bulk_upload_complete_mail" do
it "sends correctly formed email" do it "sends correctly formed email" do
expect(notify_client).to receive(:send_email).with( expect(notify_client).to receive(:send_email).with(

27
spec/models/form/lettings/pages/person_age_spec.rb

@ -1,11 +1,12 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do
subject(:page) { described_class.new(nil, page_definition, subsection, person_index:) } subject(:page) { described_class.new(nil, page_definition, subsection, person_index:, person_type:) }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) } let(:subsection) { instance_double(Form::Subsection) }
let(:person_index) { 2 } let(:person_index) { 2 }
let(:person_type) { "non_child" }
it "has correct subsection" do it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
@ -24,17 +25,33 @@ RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do
expect(page.questions.map(&:id)).to eq(%w[age2_known age2]) expect(page.questions.map(&:id)).to eq(%w[age2_known age2])
end end
context "when child" do
let(:person_type) { "child" }
it "has the correct id" do it "has the correct id" do
expect(page.id).to eq("person_2_age") expect(page.id).to eq("person_2_age_child")
end end
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq( expect(page.depends_on).to eq(
[{ "details_known_2" => 0 }], [{ "details_known_2" => 0, "person_2_child_relation?" => true }],
) )
end end
end end
context "when not child" do
it "has the correct id" do
expect(page.id).to eq("person_2_age_non_child")
end
it "has correct depends_on" do
expect(page.depends_on).to eq(
[{ "details_known_2" => 0, "person_2_child_relation?" => false }],
)
end
end
end
context "with person 3" do context "with person 3" do
let(:person_index) { 3 } let(:person_index) { 3 }
@ -43,12 +60,12 @@ RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do
end end
it "has the correct id" do it "has the correct id" do
expect(page.id).to eq("person_3_age") expect(page.id).to eq("person_3_age_non_child")
end end
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq( expect(page.depends_on).to eq(
[{ "details_known_3" => 0 }], [{ "details_known_3" => 0, "person_3_child_relation?" => false }],
) )
end end
end end

17
spec/models/form/lettings/questions/age_spec.rb

@ -1,11 +1,12 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Lettings::Questions::Age, type: :model do RSpec.describe Form::Lettings::Questions::Age, type: :model do
subject(:question) { described_class.new(nil, question_definition, page, person_index:) } subject(:question) { described_class.new(nil, question_definition, page, person_index:, person_type:) }
let(:question_definition) { nil } let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) } let(:page) { instance_double(Form::Page) }
let(:person_index) { 2 } let(:person_index) { 2 }
let(:person_type) { "non_child" }
it "has correct page" do it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -23,12 +24,22 @@ RSpec.describe Form::Lettings::Questions::Age, type: :model do
expect(question.derived?).to be false expect(question.derived?).to be false
end end
context "when child" do
let(:person_type) { "child" }
it "has the correct hint" do it "has the correct hint" do
expect(question.hint_text).to be_nil expect(question.hint_text).to eq("For a child under 1, enter 1")
end
end
context "when not child" do
it "has no hint" do
expect(question.hint_text).to be nil
end
end end
it "has the correct min" do it "has the correct min" do
expect(question.min).to eq(0) expect(question.min).to eq(1)
end end
it "has the correct max" do it "has the correct max" do

17
spec/models/form/lettings/questions/net_income_known_spec.rb

@ -0,0 +1,17 @@
require "rails_helper"
RSpec.describe Form::Lettings::Questions::NetIncomeKnown do
subject(:question) { described_class.new(nil, {}, nil) }
describe "#id" do
it "is net_income_known" do
expect(question.id).to eql("net_income_known")
end
end
describe "#type" do
it "is radio" do
expect(question.type).to eql("radio")
end
end
end

44
spec/models/form/lettings/questions/offered_social_let_spec.rb

@ -0,0 +1,44 @@
require "rails_helper"
RSpec.describe Form::Lettings::Questions::OfferedSocialLet, type: :model do
subject(:question) { described_class.new(nil, nil, page) }
let(:page) { instance_double(Form::Page) }
it "has correct page" do
expect(question.page).to eq page
end
it "has the correct id" do
expect(question.id).to eq "offered"
end
it "has the correct header" do
expect(question.header).to eq "How many times was the property offered between becoming vacant and this letting?"
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq "Times previously offered since becoming available"
end
it "has the correct type" do
expect(question.type).to eq "numeric"
end
it "has the correct minimum and maximum values" do
expect(question.min).to eq 0
expect(question.max).to eq 150
end
it "has the correct step" do
expect(question.step).to eq 1
end
it "is not marked as derived" do
expect(question.derived?).to be false
end
it "has the correct hint_text" do
expect(question.hint_text).to eq "Do not include the offer that led to this letting.This is after the last tenancy ended. If the property is being offered for let for the first time, enter 0."
end
end

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

@ -20,7 +20,7 @@ RSpec.describe Form::Lettings::Questions::PersonRelationship, type: :model do
end end
it "has the correct answer_options" do it "has the correct answer_options" do
expect(question.answer_options).to eq("C" => { "hint" => "Must be eligible for child benefit, aged under 16 or under 20 if still in full-time education.", "value" => "Child" }, expect(question.answer_options).to eq("C" => { "hint" => "Must be eligible for child benefit: under age 16 or under 20 if still in full-time education.", "value" => "Child" },
"P" => { "value" => "Partner" }, "P" => { "value" => "Partner" },
"R" => { "value" => "Person prefers not to say" }, "R" => { "value" => "Person prefers not to say" },
"X" => { "value" => "Other" }, "X" => { "value" => "Other" },

79
spec/models/form/lettings/questions/rsnvac_spec.rb

@ -0,0 +1,79 @@
require "rails_helper"
RSpec.describe Form::Lettings::Questions::Rsnvac, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("rsnvac")
end
it "has the correct header" do
expect(question.header).to eq("What is the reason for the property being vacant?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Vacancy reason")
end
it "has the correct type" do
expect(question.type).to eq("radio")
end
it "is not marked as derived" do
expect(question.derived?).to be false
end
it "has the correct answer_options" do
expect(question.answer_options).to eq({
"13" => {
"value" => "Internal transfer",
"hint" => "Excluding renewals of a fixed-term tenancy",
},
"5" => {
"value" => "Previous tenant died with no succession",
},
"9" => {
"value" => "Re-let to tenant who occupied same property as temporary accommodation",
},
"14" => {
"value" => "Renewal of fixed-term tenancy",
},
"19" => {
"value" => "Tenant involved in a succession downsize",
},
"8" => {
"value" => "Tenant moved to private sector or other accommodation",
},
"12" => {
"value" => "Tenant moved to other social housing provider",
},
"18" => {
"value" => "Tenant moved to care home",
},
"20" => {
"value" => "Tenant moved to long-stay hospital or similar institution",
},
"6" => {
"value" => "Tenant abandoned property",
},
"10" => {
"value" => "Tenant was evicted due to rent arrears",
},
"11" => {
"value" => "Tenant was evicted due to anti-social behaviour",
},
})
end
it "has the correct check_answers_card_number" do
expect(question.check_answers_card_number).to eq(0)
end
end

21
spec/models/form/lettings/subsections/household_characteristics_spec.rb

@ -36,7 +36,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
lead_tenant_over_retirement_value_check lead_tenant_over_retirement_value_check
person_2_known person_2_known
person_2_relationship_to_lead person_2_relationship_to_lead
person_2_age person_2_age_child
person_2_age_non_child
no_females_pregnant_household_person_2_age_value_check no_females_pregnant_household_person_2_age_value_check
females_in_soft_age_range_in_pregnant_household_person_2_age_value_check females_in_soft_age_range_in_pregnant_household_person_2_age_value_check
person_2_gender_identity person_2_gender_identity
@ -47,7 +48,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_2_over_retirement_value_check person_2_over_retirement_value_check
person_3_known person_3_known
person_3_relationship_to_lead person_3_relationship_to_lead
person_3_age person_3_age_child
person_3_age_non_child
no_females_pregnant_household_person_3_age_value_check no_females_pregnant_household_person_3_age_value_check
females_in_soft_age_range_in_pregnant_household_person_3_age_value_check females_in_soft_age_range_in_pregnant_household_person_3_age_value_check
person_3_gender_identity person_3_gender_identity
@ -58,7 +60,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_3_over_retirement_value_check person_3_over_retirement_value_check
person_4_known person_4_known
person_4_relationship_to_lead person_4_relationship_to_lead
person_4_age person_4_age_child
person_4_age_non_child
no_females_pregnant_household_person_4_age_value_check no_females_pregnant_household_person_4_age_value_check
females_in_soft_age_range_in_pregnant_household_person_4_age_value_check females_in_soft_age_range_in_pregnant_household_person_4_age_value_check
person_4_gender_identity person_4_gender_identity
@ -69,7 +72,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_4_over_retirement_value_check person_4_over_retirement_value_check
person_5_known person_5_known
person_5_relationship_to_lead person_5_relationship_to_lead
person_5_age person_5_age_child
person_5_age_non_child
no_females_pregnant_household_person_5_age_value_check no_females_pregnant_household_person_5_age_value_check
females_in_soft_age_range_in_pregnant_household_person_5_age_value_check females_in_soft_age_range_in_pregnant_household_person_5_age_value_check
person_5_gender_identity person_5_gender_identity
@ -80,7 +84,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_5_over_retirement_value_check person_5_over_retirement_value_check
person_6_known person_6_known
person_6_relationship_to_lead person_6_relationship_to_lead
person_6_age person_6_age_child
person_6_age_non_child
no_females_pregnant_household_person_6_age_value_check no_females_pregnant_household_person_6_age_value_check
females_in_soft_age_range_in_pregnant_household_person_6_age_value_check females_in_soft_age_range_in_pregnant_household_person_6_age_value_check
person_6_gender_identity person_6_gender_identity
@ -91,7 +96,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_6_over_retirement_value_check person_6_over_retirement_value_check
person_7_known person_7_known
person_7_relationship_to_lead person_7_relationship_to_lead
person_7_age person_7_age_child
person_7_age_non_child
no_females_pregnant_household_person_7_age_value_check no_females_pregnant_household_person_7_age_value_check
females_in_soft_age_range_in_pregnant_household_person_7_age_value_check females_in_soft_age_range_in_pregnant_household_person_7_age_value_check
person_7_gender_identity person_7_gender_identity
@ -102,7 +108,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_7_over_retirement_value_check person_7_over_retirement_value_check
person_8_known person_8_known
person_8_relationship_to_lead person_8_relationship_to_lead
person_8_age person_8_age_child
person_8_age_non_child
no_females_pregnant_household_person_8_age_value_check no_females_pregnant_household_person_8_age_value_check
females_in_soft_age_range_in_pregnant_household_person_8_age_value_check females_in_soft_age_range_in_pregnant_household_person_8_age_value_check
person_8_gender_identity person_8_gender_identity

21
spec/models/form/sales/pages/about_staircase_spec.rb

@ -1,11 +1,12 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) } subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase:) }
let(:page_id) { nil } let(:page_id) { "an_id" }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) } let(:subsection) { instance_double(Form::Subsection) }
let(:joint_purchase) { false }
it "has correct subsection" do it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
@ -32,7 +33,7 @@ RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do
end end
it "has the correct id" do it "has the correct id" do
expect(page.id).to eq("about_staircasing") expect(page.id).to eq("an_id")
end end
it "has the correct header" do it "has the correct header" do
@ -43,9 +44,23 @@ RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do
expect(page.description).to be_nil expect(page.description).to be_nil
end end
context "when not a joint purchase" do
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq([{ expect(page.depends_on).to eq([{
"staircase" => 1, "staircase" => 1,
"joint_purchase?" => false,
}]) }])
end end
end end
context "when a joint purchase" do
let(:joint_purchase) { true }
it "has correct depends_on" do
expect(page.depends_on).to eq([{
"staircase" => 1,
"joint_purchase?" => true,
}])
end
end
end

21
spec/models/form/sales/pages/buyer_previous_spec.rb

@ -1,11 +1,12 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Pages::BuyerPrevious, type: :model do RSpec.describe Form::Sales::Pages::BuyerPrevious, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) } subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase:) }
let(:page_id) { nil } let(:page_id) { "example" }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) } let(:subsection) { instance_double(Form::Subsection) }
let(:joint_purchase) { false }
it "has correct subsection" do it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
@ -16,7 +17,7 @@ RSpec.describe Form::Sales::Pages::BuyerPrevious, type: :model do
end end
it "has the correct id" do it "has the correct id" do
expect(page.id).to eq("buyer_previous") expect(page.id).to eq("example")
end end
it "has the correct header" do it "has the correct header" do
@ -26,4 +27,18 @@ RSpec.describe Form::Sales::Pages::BuyerPrevious, type: :model do
it "has the correct description" do it "has the correct description" do
expect(page.description).to be_nil expect(page.description).to be_nil
end end
context "when sales is a joint purchase" do
let(:joint_purchase) { true }
it "has the correct depends on" do
expect(page.depends_on).to eq([{ "joint_purchase?" => true }])
end
end
context "when sales is not a joint purchase" do
it "has the correct depends on" do
expect(page.depends_on).to eq([{ "joint_purchase?" => false }])
end
end
end end

19
spec/models/form/sales/pages/previous_ownership_spec.rb

@ -1,11 +1,12 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Pages::PreviousOwnership, type: :model do RSpec.describe Form::Sales::Pages::PreviousOwnership, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) } subject(:page) { described_class.new(page_id, page_definition, subsection, joint_purchase:) }
let(:page_id) { nil } let(:page_id) { "example_id" }
let(:page_definition) { nil } let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) } let(:subsection) { instance_double(Form::Subsection) }
let(:joint_purchase) { true }
it "has correct subsection" do it "has correct subsection" do
expect(page.subsection).to eq(subsection) expect(page.subsection).to eq(subsection)
@ -16,7 +17,7 @@ RSpec.describe Form::Sales::Pages::PreviousOwnership, type: :model do
end end
it "has the correct id" do it "has the correct id" do
expect(page.id).to eq("previous_ownership") expect(page.id).to eq("example_id")
end end
it "has the correct header" do it "has the correct header" do
@ -26,4 +27,16 @@ RSpec.describe Form::Sales::Pages::PreviousOwnership, type: :model do
it "has the correct description" do it "has the correct description" do
expect(page.description).to be_nil expect(page.description).to be_nil
end end
it "when sale is a joint purchase has the correct depends on" do
expect(page.depends_on).to eq([{ "joint_purchase?" => true }])
end
context "when sale is not a joint purchase" do
let(:joint_purchase) { false }
it "has the correct depends on" do
expect(page.depends_on).to eq([{ "joint_purchase?" => false }])
end
end
end end

17
spec/models/form/sales/questions/buyer_previous_spec.rb

@ -1,11 +1,12 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Questions::BuyerPrevious, type: :model do RSpec.describe Form::Sales::Questions::BuyerPrevious, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) } subject(:question) { described_class.new(question_id, question_definition, page, joint_purchase:) }
let(:question_id) { nil } let(:question_id) { nil }
let(:question_definition) { nil } let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) } let(:page) { instance_double(Form::Page) }
let(:joint_purchase) { true }
it "has correct page" do it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -15,6 +16,19 @@ RSpec.describe Form::Sales::Questions::BuyerPrevious, type: :model do
expect(question.id).to eq("soctenant") expect(question.id).to eq("soctenant")
end end
context "when a joint purchase" do
it "has the correct header" do
expect(question.header).to eq("Were any of the buyers private registered providers, housing association or local authority tenants immediately before this sale?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Any buyers were registered providers, housing association or local authority tenants immediately before this sale?")
end
end
context "when not a joint purchase" do
let(:joint_purchase) { false }
it "has the correct header" do it "has the correct header" do
expect(question.header).to eq("Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?") expect(question.header).to eq("Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?")
end end
@ -22,6 +36,7 @@ RSpec.describe Form::Sales::Questions::BuyerPrevious, type: :model do
it "has the correct check_answer_label" do it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Buyer was a registered provider, housing association or local authority tenant immediately before this sale?") expect(question.check_answer_label).to eq("Buyer was a registered provider, housing association or local authority tenant immediately before this sale?")
end end
end
it "has the correct type" do it "has the correct type" do
expect(question.type).to eq("radio") expect(question.type).to eq("radio")

17
spec/models/form/sales/questions/prevown_spec.rb

@ -1,11 +1,12 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Questions::Prevown, type: :model do RSpec.describe Form::Sales::Questions::Prevown, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) } subject(:question) { described_class.new(question_id, question_definition, page, joint_purchase:) }
let(:question_id) { nil } let(:question_id) { nil }
let(:question_definition) { nil } let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) } let(:page) { instance_double(Form::Page) }
let(:joint_purchase) { false }
it "has correct page" do it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -15,13 +16,27 @@ RSpec.describe Form::Sales::Questions::Prevown, type: :model do
expect(question.id).to eq("prevown") expect(question.id).to eq("prevown")
end end
context "when sale is not a joint purchase" do
it "has the correct header" do it "has the correct header" do
expect(question.header).to eq("Has the buyer previously owned a property?") expect(question.header).to eq("Has the buyer previously owned a property?")
end end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Buyer previously owned a property")
end
end
context "when sale is a joint purchase" do
let(:joint_purchase) { true }
it "has the correct header" do
expect(question.header).to eq("Have any of the buyers previously owned a property?")
end
it "has the correct check_answer_label" do it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Buyers previously owned a property") expect(question.check_answer_label).to eq("Buyers previously owned a property")
end end
end
it "has the correct type" do it "has the correct type" do
expect(question.type).to eq("radio") expect(question.type).to eq("radio")

17
spec/models/form/sales/questions/staircase_owned_spec.rb

@ -1,11 +1,12 @@
require "rails_helper" require "rails_helper"
RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) } subject(:question) { described_class.new(question_id, question_definition, page, joint_purchase:) }
let(:question_id) { nil } let(:question_id) { nil }
let(:question_definition) { nil } let(:question_definition) { nil }
let(:page) { instance_double(Form::Page) } let(:page) { instance_double(Form::Page) }
let(:joint_purchase) { false }
it "has correct page" do it "has correct page" do
expect(question.page).to eq(page) expect(question.page).to eq(page)
@ -15,6 +16,19 @@ RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do
expect(question.id).to eq("stairowned") expect(question.id).to eq("stairowned")
end end
context "when a joint purchase" do
let(:joint_purchase) { true }
it "has the correct header" do
expect(question.header).to eq("What percentage of the property do the buyers now own in total?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Percentage the buyers now own in total")
end
end
context "when not a joint purchase" do
it "has the correct header" do it "has the correct header" do
expect(question.header).to eq("What percentage of the property does the buyer now own in total?") expect(question.header).to eq("What percentage of the property does the buyer now own in total?")
end end
@ -22,6 +36,7 @@ RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do
it "has the correct check_answer_label" do it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Percentage the buyer now owns in total") expect(question.check_answer_label).to eq("Percentage the buyer now owns in total")
end end
end
it "has the correct type" do it "has the correct type" do
expect(question.type).to eq("numeric") expect(question.type).to eq("numeric")

18
spec/models/form/sales/subsections/household_characteristics_spec.rb

@ -195,6 +195,22 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
end end
it "has correct depends on" do it "has correct depends on" do
expect(household_characteristics.depends_on).to eq([{ "setup_completed?" => true }]) expect(household_characteristics.depends_on).to eq([{ "setup_completed?" => true, "company_buyer?" => false }])
end
context "when the sale is to a company buyer" do
let(:log) { FactoryBot.create(:sales_log, ownershipsch: 3, companybuy: 1) }
it "is not displayed in tasklist" do
expect(household_characteristics.displayed_in_tasklist?(log)).to eq(false)
end
end
context "when the sale is not to a company buyer" do
let(:log) { FactoryBot.create(:sales_log, ownershipsch: 3, companybuy: 2) }
it "is displayed in tasklist" do
expect(household_characteristics.displayed_in_tasklist?(log)).to eq(true)
end
end end
end end

6
spec/models/form/sales/subsections/income_benefits_and_savings_spec.rb

@ -35,7 +35,8 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model
savings savings
savings_value_check savings_value_check
savings_deposit_value_check savings_deposit_value_check
previous_ownership previous_ownership_joint_purchase
previous_ownership_not_joint_purchase
], ],
) )
end end
@ -62,7 +63,8 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model
savings savings
savings_value_check savings_value_check
savings_deposit_value_check savings_deposit_value_check
previous_ownership previous_ownership_joint_purchase
previous_ownership_not_joint_purchase
previous_shared previous_shared
], ],
) )

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

@ -16,14 +16,16 @@ RSpec.describe Form::Sales::Subsections::SharedOwnershipScheme, type: :model do
%w[ %w[
living_before_purchase_shared_ownership living_before_purchase_shared_ownership
staircasing staircasing
about_staircasing about_staircasing_joint_purchase
about_staircasing_not_joint_purchase
staircase_bought_value_check staircase_bought_value_check
resale resale
exchange_contracts exchange_contracts
handover_date handover_date
handover_date_check handover_date_check
la_nominations la_nominations
buyer_previous buyer_previous_joint_purchase
buyer_previous_not_joint_purchase
previous_bedrooms previous_bedrooms
previous_property_type previous_property_type
shared_ownership_previous_tenure shared_ownership_previous_tenure

159
spec/models/lettings_log_spec.rb

@ -1474,6 +1474,163 @@ RSpec.describe LettingsLog do
expect(record_from_db["rsnvac"]).to eq(14) expect(record_from_db["rsnvac"]).to eq(14)
expect(lettings_log["rsnvac"]).to eq(14) expect(lettings_log["rsnvac"]).to eq(14)
end end
context "when deriving renttype and unitletas" do
before do
allow(FeatureToggle).to receive(:startdate_two_week_validation_enabled?).and_return(false)
lettings_log.update!(rent_type:, irproduct_other: "other")
end
context "when the rent_type is Social Rent (0)" do
let(:rent_type) { 0 }
it "derives and saves renttype and unitletas as Social rent(1)" do
record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.renttype).to eq(1)
expect(record_from_db["renttype"]).to eq(1)
end
it "derives and saves unitletas as Social rent(1)" do
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(1)
expect(record_from_db["unitletas"]).to eq(1)
end
context "and it is a 23/24 form" do
it "derives and saves unitletas as Social rent(1)" do
lettings_log.update!(startdate: Time.zone.local(2023, 5, 1))
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(1)
expect(record_from_db["unitletas"]).to eq(1)
end
end
end
context "when the rent_type is Affordable Rent(1)" do
let(:rent_type) { 1 }
it "derives and saves renttype as Affordable Rent(2)" do
record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.renttype).to eq(2)
expect(record_from_db["renttype"]).to eq(2)
end
it "derives and saves unitletas as Affordable Rent(2)" do
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(2)
expect(record_from_db["unitletas"]).to eq(2)
end
context "and it is a 23/24 form" do
it "derives and saves unitletas as Affordable Rent basis(2)" do
lettings_log.update!(startdate: Time.zone.local(2023, 5, 1))
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(2)
expect(record_from_db["unitletas"]).to eq(2)
end
end
end
context "when the rent_type is London Affordable Rent(2)" do
let(:rent_type) { 2 }
it "derives and saves renttype as London Affordable Rent(2)" do
record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.renttype).to eq(2)
expect(record_from_db["renttype"]).to eq(2)
end
it "derives and saves unitletas as London Affordable Rent(2)" do
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(2)
expect(record_from_db["unitletas"]).to eq(2)
end
context "and it is a 23/24 form" do
it "derives and saves unitletas as London Affordable Rent basis(5)" do
lettings_log.update!(startdate: Time.zone.local(2023, 5, 1))
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(5)
expect(record_from_db["unitletas"]).to eq(5)
end
end
end
context "when the rent_type is Rent to Buy(3)" do
let(:rent_type) { 3 }
it "derives and saves renttype as Intermediate Rent(3)" do
record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.renttype).to eq(3)
expect(record_from_db["renttype"]).to eq(3)
end
it "derives and saves unitletas as Intermediate Rent(4)" do
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(4)
expect(record_from_db["unitletas"]).to eq(4)
end
context "and it is a 23/24 form" do
it "derives and saves unitletas as Rent to Buy basis(6)" do
lettings_log.update!(startdate: Time.zone.local(2023, 5, 1))
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(6)
expect(record_from_db["unitletas"]).to eq(6)
end
end
end
context "when the rent_type is London Living Rent(4)" do
let(:rent_type) { 4 }
it "derives and saves renttype as Intermediate Rent(3)" do
record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.renttype).to eq(3)
expect(record_from_db["renttype"]).to eq(3)
end
it "derives and saves unitletas as Intermediate Rent(4)" do
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(4)
expect(record_from_db["unitletas"]).to eq(4)
end
context "and it is a 23/24 form" do
it "derives and saves unitletas as London Living Rent basis(7)" do
lettings_log.update!(startdate: Time.zone.local(2023, 5, 1))
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(7)
expect(record_from_db["unitletas"]).to eq(7)
end
end
end
context "when the rent_type is Other intermediate rent product(5)" do
let(:rent_type) { 5 }
it "derives and saves renttype as Intermediate Rent(3)" do
record_from_db = ActiveRecord::Base.connection.execute("select renttype from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.renttype).to eq(3)
expect(record_from_db["renttype"]).to eq(3)
end
it "derives and saves unitletas as Intermediate Rent(4)" do
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(4)
expect(record_from_db["unitletas"]).to eq(4)
end
context "and it is a 23/24 form" do
it "derives and saves unitletas as Other intermediate rent basis(8)" do
lettings_log.update!(startdate: Time.zone.local(2023, 5, 1))
record_from_db = ActiveRecord::Base.connection.execute("select unitletas from lettings_logs where id=#{lettings_log.id}").to_a[0]
expect(lettings_log.unitletas).to eq(8)
expect(record_from_db["unitletas"]).to eq(8)
end
end
end
end
end end
context "when answering the household characteristics questions" do context "when answering the household characteristics questions" do
@ -1966,7 +2123,7 @@ RSpec.describe LettingsLog do
end end
context "when a non select question associated with several pages is routed to" do context "when a non select question associated with several pages is routed to" do
let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, period: 2, needstype: 1) } let(:lettings_log) { FactoryBot.create(:lettings_log, :in_progress, period: 2, needstype: 1, renewal: 0) }
it "does not clear the answer value" do it "does not clear the answer value" do
lettings_log.update!({ unitletas: 1 }) lettings_log.update!({ unitletas: 1 })

24
spec/requests/bulk_upload_lettings_logs_controller_spec.rb

@ -31,4 +31,28 @@ RSpec.describe BulkUploadLettingsLogsController, type: :request do
end end
end end
end end
describe "GET /lettings-logs/bulk-upload-logs/guidance" do
context "when not in crossover period" do
let(:expected_year) { FormHandler.instance.forms["current_lettings"].start_date.year }
it "shows guidance page with correct title" do
Timecop.freeze(2022, 1, 1) do
get "/lettings-logs/bulk-upload-logs/guidance", params: {}
expect(response.body).to include("How to upload logs in bulk")
end
end
end
context "when in crossover period" do
it "shows guidance page with correct title" do
Timecop.freeze(2023, 6, 1) do
get "/lettings-logs/bulk-upload-logs/guidance", params: {}
expect(response.body).to include("How to upload logs in bulk")
end
end
end
end
end end

24
spec/requests/bulk_upload_sales_logs_controller_spec.rb

@ -31,4 +31,28 @@ RSpec.describe BulkUploadSalesLogsController, type: :request do
end end
end end
end end
describe "GET /sales-logs/bulk-upload-logs/guidance" do
context "when not in crossover period" do
let(:expected_year) { FormHandler.instance.forms["current_sales"].start_date.year }
it "shows guidance page with correct title" do
Timecop.freeze(2022, 1, 1) do
get "/sales-logs/bulk-upload-logs/guidance", params: {}
expect(response.body).to include("How to upload logs in bulk")
end
end
end
context "when in crossover period" do
it "shows guidance page with correct title" do
Timecop.freeze(2023, 6, 1) do
get "/sales-logs/bulk-upload-logs/guidance", params: {}
expect(response.body).to include("How to upload logs in bulk")
end
end
end
end
end end

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

@ -27,63 +27,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do
} }
end end
before do let(:valid_attributes) do
create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org)
end
around do |example|
FormHandler.instance.use_real_forms!
example.run
FormHandler.instance.use_fake_forms!
end
describe "#blank_row?" do
context "when a new object" do
it "returns true" do
expect(parser).to be_blank_row
end
end
context "when any field is populated" do
before do
parser.field_1 = "1"
end
it "returns false" do
expect(parser).not_to be_blank_row
end
end
end
describe "validations" do
before do
stub_request(:get, /api.postcodes.io/)
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"Manchester\", \"codes\":{\"admin_district\": \"E08000003\"}}}", headers: {})
parser.valid?
end
describe "#valid?" do
context "when the row is blank" do
let(:attributes) { { bulk_upload: } }
it "returns true" do
expect(parser).to be_valid
end
end
context "when calling the method multiple times" do
let(:attributes) { { bulk_upload:, field_134: 2 } }
it "does not add keep adding errors to the pile" do
expect { parser.valid? }.not_to change(parser.errors, :count)
end
end
context "when valid row" do
let(:attributes) do
{ {
bulk_upload:, bulk_upload:,
field_1: "1", field_1: "1",
@ -195,6 +139,64 @@ RSpec.describe BulkUpload::Lettings::RowParser do
} }
end end
before do
create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org)
end
around do |example|
FormHandler.instance.use_real_forms!
example.run
FormHandler.instance.use_fake_forms!
end
describe "#blank_row?" do
context "when a new object" do
it "returns true" do
expect(parser).to be_blank_row
end
end
context "when any field is populated" do
before do
parser.field_1 = "1"
end
it "returns false" do
expect(parser).not_to be_blank_row
end
end
end
describe "validations" do
before do
stub_request(:get, /api.postcodes.io/)
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"Manchester\", \"codes\":{\"admin_district\": \"E08000003\"}}}", headers: {})
parser.valid?
end
describe "#valid?" do
context "when the row is blank" do
let(:attributes) { { bulk_upload: } }
it "returns true" do
expect(parser).to be_valid
end
end
context "when calling the method multiple times" do
let(:attributes) { { bulk_upload:, field_134: 2 } }
it "does not add keep adding errors to the pile" do
expect { parser.valid? }.not_to change(parser.errors, :count)
end
end
context "when valid row" do
let(:attributes) { valid_attributes }
it "returns true" do it "returns true" do
expect(parser).to be_valid expect(parser).to be_valid
end end
@ -210,6 +212,16 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end end
end end
context "when setup section not complete" do
let(:attributes) { { bulk_upload:, field_7: "123" } }
it "has errors on setup fields" do
errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute)
expect(errors).to eql(%i[field_1 field_98 field_97 field_96 field_111 field_113])
end
end
describe "#field_1" do describe "#field_1" do
context "when null" do context "when null" do
let(:attributes) { { bulk_upload:, field_1: nil, field_4: "1" } } let(:attributes) { { bulk_upload:, field_1: nil, field_4: "1" } }
@ -410,6 +422,16 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end end
end end
describe "#field_12" do
context "when set to a non-sensical value" do
let(:attributes) { valid_attributes.merge(field_12: "A", field_35: "1") }
it "returns only one error" do
expect(parser.errors[:field_12].size).to be(1)
end
end
end
describe "#field_52" do # leaving reason describe "#field_52" do # leaving reason
context "when field_134 is 1 meaning it is a renewal" do context "when field_134 is 1 meaning it is a renewal" do
context "when field_52 is 40" do context "when field_52 is 40" do
@ -519,7 +541,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end end
describe "fields 96, 97, 98 => startdate" do describe "fields 96, 97, 98 => startdate" do
context "when any one of these fields is blank" do context "when all of these fields are blank" do
let(:attributes) { { bulk_upload:, field_1: "1", field_96: nil, field_97: nil, field_98: nil } } let(:attributes) { { bulk_upload:, field_1: "1", field_96: nil, field_97: nil, field_98: nil } }
it "returns an error" do it "returns an error" do
@ -531,6 +553,18 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end end
end end
context "when one of these fields is blank" do
let(:attributes) { { bulk_upload:, field_1: "1", field_96: "1", field_97: "1", field_98: nil } }
it "returns an error only on blank field" do
parser.valid?
expect(parser.errors[:field_96]).to be_blank
expect(parser.errors[:field_97]).to be_blank
expect(parser.errors[:field_98]).to be_present
end
end
context "when field 98 is 4 digits instead of 2" do context "when field 98 is 4 digits instead of 2" do
let(:attributes) { { bulk_upload:, field_98: "2022" } } let(:attributes) { { bulk_upload:, field_98: "2022" } }
@ -648,7 +682,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do
describe "#field_134" do describe "#field_134" do
context "when an unpermitted value" do context "when an unpermitted value" do
let(:attributes) { { bulk_upload:, field_134: 3 } } let(:attributes) { { bulk_upload:, field_134: "3" } }
it "has errors on the field" do it "has errors on the field" do
expect(parser.errors[:field_134]).to be_present expect(parser.errors[:field_134]).to be_present
@ -714,6 +748,18 @@ RSpec.describe BulkUpload::Lettings::RowParser do
expect(parser.log.public_send(age)).to be(50) expect(parser.log.public_send(age)).to be(50)
end end
end end
context "when #{field} is a non-sensical value" do
let(:attributes) { { bulk_upload:, field.to_s => "A" } }
it "sets ##{known} to 0" do
expect(parser.log.public_send(known)).to be(0)
end
it "sets ##{age} to nil" do
expect(parser.log.public_send(age)).to be_nil
end
end
end end
end end

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

@ -42,7 +42,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
it "create validation error with correct values" do it "create validation error with correct values" do
validator.call validator.call
error = BulkUploadError.order(:row, :field).first error = BulkUploadError.find_by(field: "field_11")
expect(error.field).to eql("field_11") expect(error.field).to eql("field_11")
expect(error.error).to eql("You must only answer the length of the tenancy if it's fixed-term") expect(error.error).to eql("You must only answer the length of the tenancy if it's fixed-term")
@ -51,6 +51,11 @@ RSpec.describe BulkUpload::Lettings::Validator do
expect(error.row).to eql("7") expect(error.row).to eql("7")
expect(error.cell).to eql("L7") expect(error.cell).to eql("L7")
expect(error.col).to eql("L") expect(error.col).to eql("L")
expect(error.category).to be_nil
error = BulkUploadError.order(:row, :field).find_by(field: "field_111")
expect(error.category).to eql("setup")
end end
end end
@ -255,11 +260,14 @@ RSpec.describe BulkUpload::Lettings::Validator do
let(:log_5) { build(:lettings_log, renttype: 2, created_by: user, builtype: nil, startdate: Time.zone.local(2022, 5, 1)) } let(:log_5) { build(:lettings_log, renttype: 2, created_by: user, builtype: nil, startdate: Time.zone.local(2022, 5, 1)) }
before do before do
file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_csv_row) overrides = { age1: 50, age2: "R", age3: "R", age4: "4", age5: "R", age6: "R", age7: "R", age8: "R" }
file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0).to_csv_row) file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0).to_csv_row) file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0).to_csv_row) file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.close file.close
end end

39
spec/services/bulk_upload/processor_spec.rb

@ -91,6 +91,42 @@ RSpec.describe BulkUpload::Processor do
end end
end end
context "when a log has an incomplete setup section" do
let(:mock_downloader) do
instance_double(
BulkUpload::Downloader,
call: nil,
path: file_fixture("2022_23_lettings_bulk_upload.csv"),
delete_local_file!: nil,
)
end
let(:mock_validator) do
instance_double(
BulkUpload::Lettings::Validator,
invalid?: false,
call: nil,
any_setup_sections_incomplete?: true,
)
end
before do
allow(BulkUpload::Downloader).to receive(:new).with(bulk_upload:).and_return(mock_downloader)
allow(BulkUpload::Lettings::Validator).to receive(:new).and_return(mock_validator)
end
it "sends setup failure email" do
mail_double = instance_double("ActionMailer::MessageDelivery", deliver_later: nil)
allow(BulkUploadMailer).to receive(:send_bulk_upload_failed_file_setup_error_mail).and_return(mail_double)
processor.call
expect(BulkUploadMailer).to have_received(:send_bulk_upload_failed_file_setup_error_mail)
expect(mail_double).to have_received(:deliver_later)
end
end
context "when processing a bulk upload with errors but below threshold (therefore creates logs)" do context "when processing a bulk upload with errors but below threshold (therefore creates logs)" do
let(:mock_downloader) do let(:mock_downloader) do
instance_double( instance_double(
@ -106,6 +142,7 @@ RSpec.describe BulkUpload::Processor do
BulkUpload::Lettings::Validator, BulkUpload::Lettings::Validator,
invalid?: false, invalid?: false,
call: nil, call: nil,
any_setup_sections_incomplete?: false,
create_logs?: true, create_logs?: true,
) )
end end
@ -156,6 +193,7 @@ RSpec.describe BulkUpload::Processor do
BulkUpload::Lettings::Validator, BulkUpload::Lettings::Validator,
invalid?: false, invalid?: false,
call: nil, call: nil,
any_setup_sections_incomplete?: false,
create_logs?: false, create_logs?: false,
) )
end end
@ -216,6 +254,7 @@ RSpec.describe BulkUpload::Processor do
BulkUpload::Lettings::Validator, BulkUpload::Lettings::Validator,
call: nil, call: nil,
create_logs?: true, create_logs?: true,
any_setup_sections_incomplete?: false,
invalid?: false, invalid?: false,
) )
end end

54
spec/services/filter_service_spec.rb

@ -2,6 +2,7 @@ require "rails_helper"
describe FilterService do describe FilterService do
describe "filter_by_search" do describe "filter_by_search" do
context "when filtering organisations" do
before do before do
FactoryBot.create_list(:organisation, 5) FactoryBot.create_list(:organisation, 5)
FactoryBot.create(:organisation, name: "Acme LTD") FactoryBot.create(:organisation, name: "Acme LTD")
@ -25,4 +26,57 @@ describe FilterService do
end end
end end
end end
context "when filtering logs" do
context "when filtering lettings logs" do
before do
FactoryBot.create_list(:lettings_log, 5)
FactoryBot.create(:lettings_log, postcode_full: "SW1 1AA")
end
let(:lettings_log_list) { LettingsLog.all }
context "when given a postcode" do
let(:search_term) { "SW1 1AA" }
it "filters the collection on search term" do
expect(described_class.filter_by_search(lettings_log_list, search_term).count).to eq(1)
end
end
context "when not given a search term" do
let(:search_term) { nil }
it "does not filter the given collection" do
expect(described_class.filter_by_search(lettings_log_list, search_term).count).to eq(6)
end
end
end
context "when filtering sales logs" do
before do
FactoryBot.create_list(:sales_log, 5)
FactoryBot.create(:sales_log, purchid: "2")
end
let(:sales_log_list) { SalesLog.all }
context "when given a purchid" do
let(:search_term) { "2" }
it "filters the collection on search term" do
expect(described_class.filter_by_search(sales_log_list, search_term).count).to eq(1)
end
end
context "when not given a search term" do
let(:search_term) { nil }
it "does not filter the given collection" do
expect(described_class.filter_by_search(sales_log_list, search_term).count).to eq(6)
end
end
end
end
end
end end

318
spec/services/imports/sales_logs_import_service_spec.rb

@ -17,14 +17,6 @@ RSpec.describe Imports::SalesLogsImportService do
end end
before do before do
{ "GL519EX" => "E07000078",
"SW1A2AA" => "E09000033",
"SW1A1AA" => "E09000033",
"SW147QP" => "E09000027",
"B955HZ" => "E07000221" }.each do |postcode, district_code|
WebMock.stub_request(:get, /api.postcodes.io\/postcodes\/#{postcode}/).to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"#{district_code}\",\"codes\":{\"admin_district\":\"#{district_code}\"}}}", headers: {})
end
allow(Organisation).to receive(:find_by).and_return(nil) allow(Organisation).to receive(:find_by).and_return(nil)
allow(Organisation).to receive(:find_by).with(old_visible_id: organisation.old_visible_id).and_return(organisation) allow(Organisation).to receive(:find_by).with(old_visible_id: organisation.old_visible_id).and_return(organisation)
allow(Organisation).to receive(:find_by).with(old_visible_id: managing_organisation.old_visible_id).and_return(managing_organisation) allow(Organisation).to receive(:find_by).with(old_visible_id: managing_organisation.old_visible_id).and_return(managing_organisation)
@ -119,6 +111,29 @@ RSpec.describe Imports::SalesLogsImportService do
end end
end end
context "and the log startdate is before 22/23 collection period" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:DAY").content = 10
sales_log_xml.at_xpath("//xmlns:MONTH").content = 10
sales_log_xml.at_xpath("//xmlns:YEAR").content = 2021
sales_log_xml.at_xpath("//xmlns:HODAY").content = 9
sales_log_xml.at_xpath("//xmlns:HOMONTH").content = 10
sales_log_xml.at_xpath("//xmlns:HOYEAR").content = 2021
sales_log_xml.at_xpath("//xmlns:EXDAY").content = 9
sales_log_xml.at_xpath("//xmlns:EXMONTH").content = 10
sales_log_xml.at_xpath("//xmlns:EXYEAR").content = 2021
end
it "does not create the log" do
expect(logger).not_to receive(:error)
expect(logger).not_to receive(:warn)
expect { sales_log_service.send(:create_log, sales_log_xml) }
.to change(SalesLog, :count).by(0)
end
end
context "when the mortgage lender is set to an existing option" do context "when the mortgage lender is set to an existing option" do
let(:sales_log_id) { "discounted_ownership_sales_log" } let(:sales_log_id) { "discounted_ownership_sales_log" }
@ -188,6 +203,228 @@ RSpec.describe Imports::SalesLogsImportService do
end end
end end
context "and the mortgage soft validation is triggered (mortgage_value_check)" do
let(:sales_log_id) { "discounted_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:Q2Person1Income").content = "10"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the shared ownership deposit soft validation is triggered (shared_ownership_deposit_value_check)" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:DerSaleType").content = "2"
sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "275000"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the purchase price soft validation is triggered (value_value_check)" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
LaSaleRange.create!(la: "E09000033", bedrooms: 2, soft_min: 177_000, soft_max: 384_000, start_year: 2022)
sales_log_xml.at_xpath("//xmlns:Q22PurchasePrice").content = "2750"
sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "2750"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the purchase price soft validation is triggered (income1_value_check, income2_value_check)" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:Q2Person1Income").content = "20"
sales_log_xml.at_xpath("//xmlns:Q2Person2Income").content = "10"
sales_log_xml.at_xpath("//xmlns:P2Eco").content = "1"
sales_log_xml.at_xpath("//xmlns:joint").content = "1 Yes"
sales_log_xml.at_xpath("//xmlns:JointMore").content = "2 No"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the savings soft validation is triggered (savings_value_check)" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:Q3Savings").content = "200750"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the deposit soft validation is triggered (deposit_value_check)" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:Q3Savings").content = "10"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the wheelchair soft validation is triggered (wheel_value_check)" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:Q10Wheelchair").content = "1"
sales_log_xml.at_xpath("//xmlns:Disability").content = "2"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the retirement soft validation is triggered (retirement_value_check)" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:P1Eco").content = "5"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the grant soft validation is triggered (grant_value_check)" do
let(:sales_log_id) { "discounted_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:Q32Reductions").content = "5000"
sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "270000"
sales_log_xml.at_xpath("//xmlns:Q33Discount").content = ""
sales_log_xml.at_xpath("//xmlns:DerSaleType").content = "22"
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and the stairbought soft validation is triggered (staircase_bought_value_check)" do
let(:sales_log_id) { "shared_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//xmlns:PercentBought").content = "51"
sales_log_xml.at_xpath("//xmlns:PercentOwns").content = "81"
sales_log_xml.at_xpath("//xmlns:Q17aStaircase").content = "1"
sales_log_xml.at_xpath("//xmlns:Q17Resale").content = ""
sales_log_xml.at_xpath("//xmlns:EXDAY").content = ""
sales_log_xml.at_xpath("//xmlns:EXMONTH").content = ""
sales_log_xml.at_xpath("//xmlns:EXYEAR").content = ""
sales_log_xml.at_xpath("//xmlns:HODAY").content = ""
sales_log_xml.at_xpath("//xmlns:HOMONTH").content = ""
sales_log_xml.at_xpath("//xmlns:HOYEAR").content = ""
end
it "completes the log" do
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log.status).to eq("completed")
end
end
context "and it has an invalid record with invalid child, student and 16-19 age combination" do
let(:sales_log_id) { "discounted_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//meta:status").content = "submitted-invalid"
sales_log_xml.at_xpath("//xmlns:P2Age").content = 16
sales_log_xml.at_xpath("//xmlns:P2Eco").content = 7
sales_log_xml.at_xpath("//xmlns:P2Rel").content = "X"
end
it "intercepts the relevant validation error" do
expect(logger).to receive(:warn).with(/Removing field age2 from log triggering validation: Person cannot be aged 16-19 if they are a student but don't have relationship ‘child’/)
expect(logger).to receive(:warn).with(/Removing field relat2 from log triggering validation: Answer must be ‘child’ if the person is aged 16-19 and a student/)
expect(logger).to receive(:warn).with(/Removing field ecstat2 from log triggering validation: Person cannot be a student if they are aged 16-19 but don‘t have relationship ‘child’/)
expect { sales_log_service.send(:create_log, sales_log_xml) }
.not_to raise_error
end
it "clears out the invalid answers" do
allow(logger).to receive(:warn)
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log).not_to be_nil
expect(sales_log.age2).to be_nil
expect(sales_log.relat2).to be_nil
expect(sales_log.ecstat2).to be_nil
end
end
context "and it has an invalid record with invalid postcodes" do
let(:sales_log_id) { "discounted_ownership_sales_log" }
before do
sales_log_xml.at_xpath("//meta:status").content = "submitted-invalid"
sales_log_xml.at_xpath("//xmlns:Q7Postcode").content = "A1 1AA"
sales_log_xml.at_xpath("//xmlns:Q14Postcode").content = "A1 2AA"
end
it "intercepts the relevant validation error" do
expect(logger).to receive(:warn).with(/Removing field postcode_full from log triggering validation: Buyer's last accommodation and discounted ownership postcodes must match/)
expect(logger).to receive(:warn).with(/Removing field ppostcode_full from log triggering validation: Buyer's last accommodation and discounted ownership postcodes must match/)
expect(logger).to receive(:warn).with(/Removing field postcode_full from log triggering validation: postcodes_not_matching/)
expect(logger).to receive(:warn).with(/Removing field ppostcode_full from log triggering validation: postcodes_not_matching/)
expect(logger).to receive(:warn).with(/Removing postcode known and previous postcode known as the postcodes are invalid/)
expect { sales_log_service.send(:create_log, sales_log_xml) }
.not_to raise_error
end
it "clears out the invalid answers" do
allow(logger).to receive(:warn)
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log).not_to be_nil
expect(sales_log.postcode_full).to be_nil
expect(sales_log.ppostcode_full).to be_nil
end
end
context "when inferring default answers for completed sales logs" do context "when inferring default answers for completed sales logs" do
context "when the armedforcesspouse is not answered" do context "when the armedforcesspouse is not answered" do
let(:sales_log_id) { "discounted_ownership_sales_log" } let(:sales_log_id) { "discounted_ownership_sales_log" }
@ -799,6 +1036,18 @@ RSpec.describe Imports::SalesLogsImportService do
expect(sales_log&.ppostcode_full).to eq("GL51 9EX") expect(sales_log&.ppostcode_full).to eq("GL51 9EX")
expect(sales_log&.status).to eq("completed") expect(sales_log&.status).to eq("completed")
end end
it "correctly sets location fields for when location cannot be inferred from postcode" do
sales_log_xml.at_xpath("//xmlns:Q14ONSLACode").content = "E07000142"
sales_log_xml.at_xpath("//xmlns:Q14Postcode").content = "A11AA"
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log&.pcodenk).to eq(0) # postcode known
expect(sales_log&.la_known).to eq(1) # la known
expect(sales_log&.la).to eq("E07000142")
expect(sales_log&.status).to eq("completed")
end
end end
context "when setting default buyer 1 previous tenancy" do context "when setting default buyer 1 previous tenancy" do
@ -824,6 +1073,59 @@ RSpec.describe Imports::SalesLogsImportService do
expect(sales_log&.prevten).to eq(2) expect(sales_log&.prevten).to eq(2)
end end
end end
context "when mortgage used is don't know" do
let(:sales_log_id) { "discounted_ownership_sales_log" }
before do
allow(logger).to receive(:warn).and_return(nil)
end
it "sets mortgageused to don't know if mortlen, mortgage and extrabor are blank" do
sales_log_xml.at_xpath("//xmlns:MORTGAGEUSED").content = "3 Don't know"
sales_log_xml.at_xpath("//xmlns:Q35Borrowing").content = ""
sales_log_xml.at_xpath("//xmlns:Q34b").content = ""
sales_log_xml.at_xpath("//xmlns:CALCMORT").content = ""
sales_log_xml.at_xpath("//xmlns:Q36CashDeposit").content = "134750"
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log&.mortgageused).to eq(3)
end
it "sets mortgageused to yes if mortgage is given" do
sales_log_xml.at_xpath("//xmlns:MORTGAGEUSED").content = "3 Don't know"
sales_log_xml.at_xpath("//xmlns:Q35Borrowing").content = ""
sales_log_xml.at_xpath("//xmlns:Q34b").content = ""
sales_log_xml.at_xpath("//xmlns:CALCMORT").content = "134750"
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log&.mortgageused).to eq(1)
end
it "sets mortgageused to yes if mortlen is given" do
sales_log_xml.at_xpath("//xmlns:MORTGAGEUSED").content = "3 Don't know"
sales_log_xml.at_xpath("//xmlns:Q35Borrowing").content = ""
sales_log_xml.at_xpath("//xmlns:Q34b").content = "10"
sales_log_xml.at_xpath("//xmlns:CALCMORT").content = ""
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log&.mortgageused).to eq(1)
end
it "sets mortgageused to yes if extrabor is given" do
sales_log_xml.at_xpath("//xmlns:MORTGAGEUSED").content = "3 Don't know"
sales_log_xml.at_xpath("//xmlns:Q35Borrowing").content = "3000"
sales_log_xml.at_xpath("//xmlns:Q34b").content = ""
sales_log_xml.at_xpath("//xmlns:CALCMORT").content = ""
sales_log_service.send(:create_log, sales_log_xml)
sales_log = SalesLog.find_by(old_id: sales_log_id)
expect(sales_log&.mortgageused).to eq(1)
end
end
end end
end end
end end

16
spec/support/bulk_upload/log_to_csv.rb

@ -22,14 +22,14 @@ class BulkUpload::LogToCsv
log.tenancy, log.tenancy,
log.tenancyother, # 10 log.tenancyother, # 10
log.tenancylength, log.tenancylength,
log.age1, log.age1 || overrides[:age1],
log.age2, log.age2 || overrides[:age2],
log.age3, log.age3 || overrides[:age3],
log.age4, log.age4 || overrides[:age4],
log.age5, log.age5 || overrides[:age5],
log.age6, log.age6 || overrides[:age6],
log.age7, log.age7 || overrides[:age7],
log.age8, log.age8 || overrides[:age8],
log.sex1, # 20 log.sex1, # 20
log.sex2, log.sex2,

Loading…
Cancel
Save