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. 36
      app/services/bulk_upload/processor.rb
  37. 25
      app/services/imports/local_authorities_service.rb
  38. 70
      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. 35
      spec/models/form/lettings/pages/person_age_spec.rb
  59. 19
      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. 29
      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. 25
      spec/models/form/sales/questions/buyer_previous_spec.rb
  69. 25
      spec/models/form/sales/questions/prevown_spec.rb
  70. 25
      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. 272
      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. 80
      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
gem "paper_trail-globalid"
# Request rate limiting
gem "rack", ">= 2.2.6.3"
gem "rack-attack"
gem "redis", "~> 4.8"
# Receive exceptions and configure alerts

3
Gemfile.lock

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

2
app/components/search_component.rb

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

5
app/controllers/bulk_upload_lettings_logs_controller.rb

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

5
app/controllers/bulk_upload_sales_logs_controller.rb

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

16
app/helpers/logs_helper.rb

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

39
app/mailers/bulk_upload_mailer.rb

@ -66,17 +66,40 @@ class BulkUploadMailer < NotifyMailer
)
end
def send_bulk_upload_failed_file_setup_error_mail(user, bulk_upload)
def send_bulk_upload_failed_file_setup_error_mail(bulk_upload:)
bulk_upload_link = if bulk_upload.lettings?
start_bulk_upload_lettings_logs_url
else
start_bulk_upload_sales_logs_url
end
validator_class = if bulk_upload.lettings?
BulkUpload::Lettings::Validator
else
BulkUpload::Sales::Validator
end
errors = bulk_upload
.bulk_upload_errors
.where(category: "setup")
.group(:col, :field)
.count
.keys
.sort_by { |_col, field| field }
.map do |col, field|
"- Column #{col} (#{validator_class.question_for_field(field.to_sym)})"
end
send_email(
user.email,
bulk_upload.user.email,
BULK_UPLOAD_FAILED_FILE_SETUP_ERROR_TEMPLATE_ID,
{
filename: "[#{bulk_upload} filename]",
upload_timestamp: "[#{bulk_upload} upload_timestamp]",
lettings_or_sales: "[#{bulk_upload} lettings_or_sales]",
year_combo: "[#{bulk_upload} year_combo]",
errors_list: "[#{bulk_upload} errors_list]",
bulk_upload_link: "[#{bulk_upload} bulk_upload_link]",
filename: bulk_upload.filename,
upload_timestamp: bulk_upload.created_at.to_fs(:govuk_date_and_time),
lettings_or_sales: bulk_upload.log_type,
year_combo: bulk_upload.year_combo,
errors_list: errors.join("\n"),
bulk_upload_link:,
},
)
end

29
app/models/derived_variables/lettings_log_variables.rb

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,9 +1,9 @@
class Form::Sales::Questions::Prevown < ::Form::Question
def initialize(id, hsh, page)
super
def initialize(id, hsh, page, joint_purchase:)
super(id, hsh, page)
@id = "prevown"
@check_answer_label = "Buyers previously owned a property"
@header = "Has the buyer previously owned a property?"
@check_answer_label = I18n.t("check_answer_labels.prevown", count: joint_purchase ? 2 : 1)
@header = I18n.t("questions.prevown", count: joint_purchase ? 2 : 1)
@type = "radio"
@answer_options = ANSWER_OPTIONS
@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
@id = "la_known"
@check_answer_label = "Local authority known"
@header = "Do you know the local authority of the property?"
@header = "Do you know the property’s local authority?"
@type = "radio"
@answer_options = ANSWER_OPTIONS
@conditional_for = { "la" => [1] }

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

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

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

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

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::SavingsValueCheck.new("savings_value_check", nil, self),
Form::Sales::Pages::DepositValueCheck.new("savings_deposit_value_check", nil, self),
Form::Sales::Pages::PreviousOwnership.new(nil, nil, self),
Form::Sales::Pages::PreviousOwnership.new("previous_ownership_joint_purchase", nil, self, joint_purchase: true),
Form::Sales::Pages::PreviousOwnership.new("previous_ownership_not_joint_purchase", nil, self, joint_purchase: false),
previous_shared_page,
].compact
end

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

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

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

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

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

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

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

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

11
app/models/sales_log.rb

@ -33,7 +33,12 @@ class SalesLog < Log
before_validation :set_derived_fields!
scope :filter_by_year, ->(year) { where(saledate: Time.zone.local(year.to_i, 4, 1)...Time.zone.local(year.to_i + 1, 4, 1)) }
scope :search_by, ->(param) { filter_by_id(param) }
scope :filter_by_purchaser_code, ->(purchid) { where("purchid ILIKE ?", "%#{purchid}%") }
scope :search_by, lambda { |param|
filter_by_purchaser_code(param)
.or(filter_by_postcode(param))
.or(filter_by_id(param))
}
scope :filter_by_organisation, ->(org, _user = nil) { where(owning_organisation: org) }
OPTIONAL_FIELDS = %w[saledate_check purchid monthly_charges_value_check old_persons_shared_ownership_value_check].freeze
@ -257,6 +262,10 @@ class SalesLog < Log
ownershipsch == 1
end
def company_buyer?
companybuy == 1
end
def buyers_age_for_old_persons_shared_ownership_invalid?
return unless old_persons_shared_ownership?

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

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

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") },
inclusion: { in: (1..12).to_a, message: I18n.t("validations.invalid_option", question: "letting type") }
validates :field_4, presence: { if: proc { [2, 4, 6, 8, 10, 12].include?(field_1) } }
validates :field_12, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 1 must be a number or the letter R" }
validates :field_13, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 2 must be a number or the letter R" }
validates :field_14, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 3 must be a number or the letter R" }
validates :field_15, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 4 must be a number or the letter R" }
validates :field_16, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 5 must be a number or the letter R" }
validates :field_17, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 6 must be a number or the letter R" }
validates :field_18, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 7 must be a number or the letter R" }
validates :field_19, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 8 must be a number or the letter R" }
validates :field_96, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (day)") }
validates :field_97, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (month)") }
validates :field_98, presence: { message: I18n.t("validations.not_answered", question: "tenancy start date (year)") }
validates :field_98, format: { with: /\A\d{2}\z/, message: I18n.t("validations.setup.startdate.year_not_two_digits") }
validate :validate_data_types
@ -181,7 +195,12 @@ class BulkUpload::Lettings::RowParser
log.errors.each do |error|
fields = field_mapping_for_errors[error.attribute] || []
fields.each { |field| errors.add(field, error.type) }
fields.each do |field|
unless errors.include?(field)
errors.add(field, error.type)
end
end
end
errors.blank?
@ -203,6 +222,10 @@ class BulkUpload::Lettings::RowParser
block_log_creation
end
def setup_section_incomplete?
log.form.setup_sections[0].subsections[0].is_incomplete?(log)
end
private
def validate_location_related
@ -222,7 +245,7 @@ private
def validate_location_exists
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
@ -240,7 +263,7 @@ private
def validate_scheme_exists
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
@ -254,7 +277,7 @@ private
def validate_managing_org_exists
if managing_organisation.nil?
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
@ -269,7 +292,7 @@ private
def validate_owning_org_exists
if owning_organisation.nil?
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
@ -337,7 +360,7 @@ private
end
def validate_relevant_collection_window
return unless start_date && bulk_upload.form
return if start_date.blank? || bulk_upload.form.blank?
unless bulk_upload.form.valid_start_date_for_form?(start_date)
errors.add(:field_96, I18n.t("validations.date.outside_collection_window"))
@ -347,6 +370,8 @@ private
end
def start_date
return if field_98.blank? || field_97.blank? || field_96.blank?
Date.parse("20#{field_98.to_s.rjust(2, '0')}-#{field_97}-#{field_96}")
rescue StandardError
nil
@ -387,10 +412,26 @@ private
next if log.optional_fields.include?(question.id)
next if question.completed?(log)
fields.each { |field| errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase)) }
if setup_question?(question)
fields.each do |field|
if errors[field].present?
errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase), category: :setup)
end
end
else
fields.each do |field|
unless errors.any? { |e| fields.include?(e.attribute) }
errors.add(field, I18n.t("validations.not_answered", question: question.check_answer_label&.downcase))
end
end
end
end
end
def setup_question?(question)
log.form.setup_sections[0].subsections[0].questions.include?(question)
end
def field_mapping_for_errors
{
lettype: [:field_1],
@ -398,6 +439,8 @@ private
postcode_known: %i[field_107 field_108 field_109],
postcode_full: %i[field_107 field_108 field_109],
la: %i[field_107],
owning_organisation: [:field_111],
managing_organisation: [:field_113],
owning_organisation_id: [:field_111],
managing_organisation_id: [:field_113],
renewal: [:field_134],
@ -628,28 +671,28 @@ private
attributes["declaration"] = field_132
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"] = 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"] = 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"] = 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"] = 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"] = 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"] = 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"] = 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["sex2"] = field_21
@ -868,6 +911,8 @@ private
0
when nil
rsnvac == 14 ? 1 : 0
else
field_134
end
end

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

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

36
app/services/bulk_upload/processor.rb

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

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

70
app/services/imports/sales_logs_import_service.rb

@ -16,7 +16,9 @@ module Imports
private
def create_log(xml_doc)
# only import sales logs from 22/23 collection period onwards
return unless meta_field_value(xml_doc, "form-name").include?("Sales")
return unless compose_date(xml_doc, "DAY", "MONTH", "YEAR") >= Time.zone.local(2022, 4, 1)
attributes = {}
@ -76,7 +78,7 @@ module Imports
attributes["inc2mort"] = unsafe_string_as_integer(xml_doc, "Q2Person2MortApplication")
attributes["hb"] = unsafe_string_as_integer(xml_doc, "Q2a")
attributes["frombeds"] = safe_string_as_integer(xml_doc, "Q20Bedrooms")
attributes["staircase"] = unsafe_string_as_integer(xml_doc, "Q17aStaircase")
attributes["staircase"] = unsafe_string_as_integer(xml_doc, "Q17aStaircase") if attributes["ownershipsch"] == 1
attributes["stairbought"] = safe_string_as_integer(xml_doc, "PercentBought")
attributes["stairowned"] = safe_string_as_integer(xml_doc, "PercentOwns") if attributes["staircase"] == 1
attributes["mrent"] = safe_string_as_decimal(xml_doc, "Q28MonthlyRent")
@ -102,7 +104,6 @@ module Imports
attributes["ppcodenk"] = previous_postcode_known(xml_doc, attributes["ppostcode_full"], attributes["prevloc"]) # Q7UNKNOWNPOSTCODE check mapping
attributes["ppostc1"] = string_or_nil(xml_doc, "PPOSTC1")
attributes["ppostc2"] = string_or_nil(xml_doc, "PPOSTC2")
attributes["previous_la_known"] = nil
attributes["hhregres"] = unsafe_string_as_integer(xml_doc, "ArmedF")
attributes["hhregresstill"] = still_serving(xml_doc)
attributes["proplen"] = safe_string_as_integer(xml_doc, "Q16aProplen2") || safe_string_as_integer(xml_doc, "Q16aProplensec2")
@ -111,7 +112,7 @@ module Imports
attributes["prevten"] = unsafe_string_as_integer(xml_doc, "Q6PrevTenure")
attributes["mortlen"] = mortgage_length(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["armedforcesspouse"] = unsafe_string_as_integer(xml_doc, "ARMEDFORCESSPOUSE")
attributes["hodate"] = compose_date(xml_doc, "HODAY", "HOMONTH", "HOYEAR")
@ -130,13 +131,29 @@ module Imports
attributes["prevshared"] = 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["ppostcode_full"].blank?
if attributes["la"].present? && attributes["postcode_full"].blank?
attributes["previous_la_known"] = 1 if attributes["prevloc"].present?
if attributes["la"].present?
attributes["la_known"] = 1
attributes["is_la_inferred"] = false
end
# Soft validations can become required answers, set them to yes by default
attributes["mortgage_value_check"] = 0
attributes["shared_ownership_deposit_value_check"] = 0
attributes["value_value_check"] = 0
attributes["savings_value_check"] = 0
attributes["income1_value_check"] = 0
attributes["deposit_value_check"] = 0
attributes["wheel_value_check"] = 0
attributes["retirement_value_check"] = 0
attributes["extrabor_value_check"] = 0
attributes["grant_value_check"] = 0
attributes["staircase_bought_value_check"] = 0
attributes["deposit_and_mortgage_value_check"] = 0
attributes["old_persons_shared_ownership_value_check"] = 0
attributes["income2_value_check"] = 0
attributes["monthly_charges_value_check"] = 0
# Sets the log creator
owner_id = meta_field_value(xml_doc, "owner-user-id").strip
if owner_id.present?
@ -168,9 +185,31 @@ module Imports
end
end
def rescue_validation_or_raise(sales_log, _attributes, _previous_status, exception)
@logger.error("Log #{sales_log.old_id}: Failed to import")
raise 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")
sales_log.errors.each do |error|
@logger.error("Validation error: Field #{error.attribute}:")
@logger.error("\tOwning Organisation: #{sales_log.owning_organisation&.name}")
@logger.error("\tOld CORE ID: #{sales_log.old_id}")
@logger.error("\tOld CORE: #{attributes[error.attribute.to_s]&.inspect}")
@logger.error("\tNew CORE: #{sales_log.read_attribute(error.attribute)&.inspect}")
@logger.error("\tError message: #{error.type}")
end
raise exception
end
end
def compute_differences(sales_log, attributes)
@ -189,6 +228,7 @@ module Imports
def fields_not_present_in_softwire_data
%w[created_by
income1_value_check
income2_value_check
mortgage_value_check
savings_value_check
deposit_value_check
@ -388,7 +428,6 @@ module Imports
end
def monthly_charges(xml_doc, attributes)
safe_string_as_decimal(xml_doc, "Q29MonthlyCharges")
case attributes["ownershipsch"]
when 1
safe_string_as_decimal(xml_doc, "Q29MonthlyCharges")
@ -446,6 +485,17 @@ module Imports
UKPostcode.parse(postcode).to_s
end
def mortgage_used(xml_doc, attributes)
mortgageused = unsafe_string_as_integer(xml_doc, "MORTGAGEUSED")
return mortgageused unless mortgageused == 3
if attributes["mortgage"].present? || attributes["mortlen"].present? || attributes["extrabor"].present?
1 # yes
else
3 # don't know
end
end
def set_default_values(attributes)
attributes["armedforcesspouse"] ||= 7
attributes["hhregres"] ||= 8

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

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

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

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

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

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

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

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

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

2
config/routes.rb

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

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.
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
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 "updated_at", null: false
t.text "col"
t.text "category"
t.index ["bulk_upload_id"], name: "index_bulk_upload_errors_on_bulk_upload_id"
end
@ -288,6 +289,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_01_144555) do
t.index ["updated_by_id"], name: "index_lettings_logs_on_updated_by_id"
end
create_table "local_authorities", force: :cascade do |t|
t.string "code", null: false
t.string "name", null: false
t.datetime "start_date", null: false
t.datetime "end_date"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["code"], name: "index_local_authority_code", unique: true
end
create_table "location_deactivation_periods", force: :cascade do |t|
t.datetime "deactivation_date"
t.datetime "reactivation_date"

6
db/seeds.rb

@ -325,4 +325,10 @@ unless Rails.env.test?
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

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 }
type { 8 }
saledate { Time.utc(2023, 2, 2, 10, 36, 49) }
companybuy { 1 }
jointpur { 1 }
beds { 2 }
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>
<P8RRefused>0</P8RRefused>
<TotRRefused>0</TotRRefused>
<CALCMORT>76000</CALCMORT>
<CALCMORT></CALCMORT>
<MORTGAGEUSED>1</MORTGAGEUSED>
<IM1>47000</IM1>
<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)
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
it "sends correctly formed email" do
expect(notify_client).to receive(:send_email).with(

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

@ -1,11 +1,12 @@
require "rails_helper"
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(:subsection) { instance_double(Form::Subsection) }
let(:person_index) { 2 }
let(:person_type) { "non_child" }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
@ -24,14 +25,30 @@ RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do
expect(page.questions.map(&:id)).to eq(%w[age2_known age2])
end
it "has the correct id" do
expect(page.id).to eq("person_2_age")
context "when child" do
let(:person_type) { "child" }
it "has the correct id" do
expect(page.id).to eq("person_2_age_child")
end
it "has correct depends_on" do
expect(page.depends_on).to eq(
[{ "details_known_2" => 0, "person_2_child_relation?" => true }],
)
end
end
it "has correct depends_on" do
expect(page.depends_on).to eq(
[{ "details_known_2" => 0 }],
)
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
@ -43,12 +60,12 @@ RSpec.describe Form::Lettings::Pages::PersonAge, type: :model do
end
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
it "has correct depends_on" do
expect(page.depends_on).to eq(
[{ "details_known_3" => 0 }],
[{ "details_known_3" => 0, "person_3_child_relation?" => false }],
)
end
end

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

@ -1,11 +1,12 @@
require "rails_helper"
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(:page) { instance_double(Form::Page) }
let(:person_index) { 2 }
let(:person_type) { "non_child" }
it "has correct page" do
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
end
it "has the correct hint" do
expect(question.hint_text).to be_nil
context "when child" do
let(:person_type) { "child" }
it "has the correct hint" do
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
it "has the correct min" do
expect(question.min).to eq(0)
expect(question.min).to eq(1)
end
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
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" },
"R" => { "value" => "Person prefers not to say" },
"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
person_2_known
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
females_in_soft_age_range_in_pregnant_household_person_2_age_value_check
person_2_gender_identity
@ -47,7 +48,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_2_over_retirement_value_check
person_3_known
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
females_in_soft_age_range_in_pregnant_household_person_3_age_value_check
person_3_gender_identity
@ -58,7 +60,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_3_over_retirement_value_check
person_4_known
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
females_in_soft_age_range_in_pregnant_household_person_4_age_value_check
person_4_gender_identity
@ -69,7 +72,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_4_over_retirement_value_check
person_5_known
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
females_in_soft_age_range_in_pregnant_household_person_5_age_value_check
person_5_gender_identity
@ -80,7 +84,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_5_over_retirement_value_check
person_6_known
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
females_in_soft_age_range_in_pregnant_household_person_6_age_value_check
person_6_gender_identity
@ -91,7 +96,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_6_over_retirement_value_check
person_7_known
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
females_in_soft_age_range_in_pregnant_household_person_7_age_value_check
person_7_gender_identity
@ -102,7 +108,8 @@ RSpec.describe Form::Lettings::Subsections::HouseholdCharacteristics, type: :mod
person_7_over_retirement_value_check
person_8_known
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
females_in_soft_age_range_in_pregnant_household_person_8_age_value_check
person_8_gender_identity

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

@ -1,11 +1,12 @@
require "rails_helper"
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(:subsection) { instance_double(Form::Subsection) }
let(:joint_purchase) { false }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
@ -32,7 +33,7 @@ RSpec.describe Form::Sales::Pages::AboutStaircase, type: :model do
end
it "has the correct id" do
expect(page.id).to eq("about_staircasing")
expect(page.id).to eq("an_id")
end
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
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{
"staircase" => 1,
}])
context "when not a joint purchase" do
it "has correct depends_on" do
expect(page.depends_on).to eq([{
"staircase" => 1,
"joint_purchase?" => false,
}])
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"
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(:subsection) { instance_double(Form::Subsection) }
let(:joint_purchase) { false }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
@ -16,7 +17,7 @@ RSpec.describe Form::Sales::Pages::BuyerPrevious, type: :model do
end
it "has the correct id" do
expect(page.id).to eq("buyer_previous")
expect(page.id).to eq("example")
end
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
expect(page.description).to be_nil
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

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

@ -1,11 +1,12 @@
require "rails_helper"
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(:subsection) { instance_double(Form::Subsection) }
let(:joint_purchase) { true }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
@ -16,7 +17,7 @@ RSpec.describe Form::Sales::Pages::PreviousOwnership, type: :model do
end
it "has the correct id" do
expect(page.id).to eq("previous_ownership")
expect(page.id).to eq("example_id")
end
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
expect(page.description).to be_nil
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

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

@ -1,11 +1,12 @@
require "rails_helper"
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_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:joint_purchase) { true }
it "has correct page" do
expect(question.page).to eq(page)
@ -15,12 +16,26 @@ RSpec.describe Form::Sales::Questions::BuyerPrevious, type: :model do
expect(question.id).to eq("soctenant")
end
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?")
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
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?")
context "when not a joint purchase" do
let(:joint_purchase) { false }
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?")
end
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?")
end
end
it "has the correct type" do

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

@ -1,11 +1,12 @@
require "rails_helper"
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_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:joint_purchase) { false }
it "has correct page" do
expect(question.page).to eq(page)
@ -15,12 +16,26 @@ RSpec.describe Form::Sales::Questions::Prevown, type: :model do
expect(question.id).to eq("prevown")
end
it "has the correct header" do
expect(question.header).to eq("Has the buyer previously owned a property?")
context "when sale is not a joint purchase" do
it "has the correct header" do
expect(question.header).to eq("Has the buyer previously owned a property?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Buyer previously owned a property")
end
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Buyers previously owned a property")
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
expect(question.check_answer_label).to eq("Buyers previously owned a property")
end
end
it "has the correct type" do

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

@ -1,11 +1,12 @@
require "rails_helper"
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_definition) { nil }
let(:page) { instance_double(Form::Page) }
let(:joint_purchase) { false }
it "has correct page" do
expect(question.page).to eq(page)
@ -15,12 +16,26 @@ RSpec.describe Form::Sales::Questions::StaircaseOwned, type: :model do
expect(question.id).to eq("stairowned")
end
it "has the correct header" do
expect(question.header).to eq("What percentage of the property does the buyer now own in total?")
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
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Percentage the buyer now owns in total")
context "when not a joint purchase" do
it "has the correct header" do
expect(question.header).to eq("What percentage of the property does the buyer now own in total?")
end
it "has the correct check_answer_label" do
expect(question.check_answer_label).to eq("Percentage the buyer now owns in total")
end
end
it "has the correct type" do

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

@ -195,6 +195,22 @@ RSpec.describe Form::Sales::Subsections::HouseholdCharacteristics, type: :model
end
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

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_value_check
savings_deposit_value_check
previous_ownership
previous_ownership_joint_purchase
previous_ownership_not_joint_purchase
],
)
end
@ -62,7 +63,8 @@ RSpec.describe Form::Sales::Subsections::IncomeBenefitsAndSavings, type: :model
savings
savings_value_check
savings_deposit_value_check
previous_ownership
previous_ownership_joint_purchase
previous_ownership_not_joint_purchase
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[
living_before_purchase_shared_ownership
staircasing
about_staircasing
about_staircasing_joint_purchase
about_staircasing_not_joint_purchase
staircase_bought_value_check
resale
exchange_contracts
handover_date
handover_date_check
la_nominations
buyer_previous
buyer_previous_joint_purchase
buyer_previous_not_joint_purchase
previous_bedrooms
previous_property_type
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(lettings_log["rsnvac"]).to eq(14)
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
context "when answering the household characteristics questions" do
@ -1966,7 +2123,7 @@ RSpec.describe LettingsLog do
end
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
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
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

24
spec/requests/bulk_upload_sales_logs_controller_spec.rb

@ -31,4 +31,28 @@ RSpec.describe BulkUploadSalesLogsController, type: :request do
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

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

@ -27,6 +27,118 @@ RSpec.describe BulkUpload::Lettings::RowParser do
}
end
let(:valid_attributes) do
{
bulk_upload:,
field_1: "1",
field_4: scheme.old_visible_id,
field_7: "123",
field_96: now.day.to_s,
field_97: now.month.to_s,
field_98: now.strftime("%g"),
field_108: "EC1N",
field_109: "2TD",
field_111: owning_org.old_visible_id,
field_113: managing_org.old_visible_id,
field_130: "1",
field_134: "2",
field_102: "2",
field_103: "1",
field_104: "1",
field_101: "1",
field_133: "2",
field_8: "1",
field_9: "2",
field_132: "1",
field_12: "42",
field_13: "41",
field_14: "20",
field_15: "18",
field_16: "16",
field_17: "14",
field_18: "12",
field_19: "20",
field_20: "F",
field_21: "M",
field_22: "F",
field_23: "M",
field_24: "F",
field_25: "M",
field_26: "F",
field_27: "M",
field_43: "17",
field_44: "18",
field_28: "P",
field_29: "C",
field_30: "X",
field_31: "R",
field_32: "C",
field_33: "C",
field_34: "X",
field_35: "1",
field_36: "2",
field_37: "6",
field_38: "7",
field_39: "8",
field_40: "9",
field_41: "0",
field_42: "10",
field_45: "1",
field_114: "4",
field_46: "1",
field_47: "1",
field_118: "2",
field_66: "5",
field_67: "2",
field_52: "31",
field_61: "3",
field_68: "12",
field_65: "1",
field_63: "EC1N",
field_64: "2TD",
field_69: "1",
field_70: "1",
field_71: "",
field_72: "1",
field_73: "",
field_74: "",
field_75: "1",
field_76: "2",
field_77: "2",
field_78: "2",
field_51: "1",
field_50: "2000",
field_116: "2",
field_48: "1",
field_49: "1",
field_79: "4",
field_80: "1234.56",
field_87: "1",
field_88: "234.56",
field_106: "15",
field_99: "0",
field_89: now.day.to_s,
field_90: now.month.to_s,
field_91: now.strftime("%g"),
}
end
before do
create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org)
end
@ -83,117 +195,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end
context "when valid row" do
let(:attributes) do
{
bulk_upload:,
field_1: "1",
field_4: scheme.old_visible_id,
field_7: "123",
field_96: now.day.to_s,
field_97: now.month.to_s,
field_98: now.strftime("%g"),
field_108: "EC1N",
field_109: "2TD",
field_111: owning_org.old_visible_id,
field_113: managing_org.old_visible_id,
field_130: "1",
field_134: "2",
field_102: "2",
field_103: "1",
field_104: "1",
field_101: "1",
field_133: "2",
field_8: "1",
field_9: "2",
field_132: "1",
field_12: "42",
field_13: "41",
field_14: "20",
field_15: "18",
field_16: "16",
field_17: "14",
field_18: "12",
field_19: "20",
field_20: "F",
field_21: "M",
field_22: "F",
field_23: "M",
field_24: "F",
field_25: "M",
field_26: "F",
field_27: "M",
field_43: "17",
field_44: "18",
field_28: "P",
field_29: "C",
field_30: "X",
field_31: "R",
field_32: "C",
field_33: "C",
field_34: "X",
field_35: "1",
field_36: "2",
field_37: "6",
field_38: "7",
field_39: "8",
field_40: "9",
field_41: "0",
field_42: "10",
field_45: "1",
field_114: "4",
field_46: "1",
field_47: "1",
field_118: "2",
field_66: "5",
field_67: "2",
field_52: "31",
field_61: "3",
field_68: "12",
field_65: "1",
field_63: "EC1N",
field_64: "2TD",
field_69: "1",
field_70: "1",
field_71: "",
field_72: "1",
field_73: "",
field_74: "",
field_75: "1",
field_76: "2",
field_77: "2",
field_78: "2",
field_51: "1",
field_50: "2000",
field_116: "2",
field_48: "1",
field_49: "1",
field_79: "4",
field_80: "1234.56",
field_87: "1",
field_88: "234.56",
field_106: "15",
field_99: "0",
field_89: now.day.to_s,
field_90: now.month.to_s,
field_91: now.strftime("%g"),
}
end
let(:attributes) { valid_attributes }
it "returns true" do
expect(parser).to be_valid
@ -210,6 +212,16 @@ RSpec.describe BulkUpload::Lettings::RowParser do
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
context "when null" do
let(:attributes) { { bulk_upload:, field_1: nil, field_4: "1" } }
@ -410,6 +422,16 @@ RSpec.describe BulkUpload::Lettings::RowParser do
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
context "when field_134 is 1 meaning it is a renewal" do
context "when field_52 is 40" do
@ -519,7 +541,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do
end
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 } }
it "returns an error" do
@ -531,6 +553,18 @@ RSpec.describe BulkUpload::Lettings::RowParser do
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
let(:attributes) { { bulk_upload:, field_98: "2022" } }
@ -648,7 +682,7 @@ RSpec.describe BulkUpload::Lettings::RowParser do
describe "#field_134" 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
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)
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

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
validator.call
error = BulkUploadError.order(:row, :field).first
error = BulkUploadError.find_by(field: "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")
@ -51,6 +51,11 @@ RSpec.describe BulkUpload::Lettings::Validator do
expect(error.row).to eql("7")
expect(error.cell).to eql("L7")
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
@ -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)) }
before do
file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0).to_csv_row)
overrides = { age1: 50, age2: "R", age3: "R", age4: "4", age5: "R", age6: "R", age7: "R", age8: "R" }
file.write(BulkUpload::LogToCsv.new(log: log_1, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_2, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_3, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_4, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.write(BulkUpload::LogToCsv.new(log: log_5, line_ending: "\r\n", col_offset: 0, overrides:).to_csv_row)
file.close
end

39
spec/services/bulk_upload/processor_spec.rb

@ -91,6 +91,42 @@ RSpec.describe BulkUpload::Processor do
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
let(:mock_downloader) do
instance_double(
@ -106,6 +142,7 @@ RSpec.describe BulkUpload::Processor do
BulkUpload::Lettings::Validator,
invalid?: false,
call: nil,
any_setup_sections_incomplete?: false,
create_logs?: true,
)
end
@ -156,6 +193,7 @@ RSpec.describe BulkUpload::Processor do
BulkUpload::Lettings::Validator,
invalid?: false,
call: nil,
any_setup_sections_incomplete?: false,
create_logs?: false,
)
end
@ -216,6 +254,7 @@ RSpec.describe BulkUpload::Processor do
BulkUpload::Lettings::Validator,
call: nil,
create_logs?: true,
any_setup_sections_incomplete?: false,
invalid?: false,
)
end

80
spec/services/filter_service_spec.rb

@ -2,26 +2,80 @@ require "rails_helper"
describe FilterService do
describe "filter_by_search" do
before do
FactoryBot.create_list(:organisation, 5)
FactoryBot.create(:organisation, name: "Acme LTD")
end
context "when filtering organisations" do
before do
FactoryBot.create_list(:organisation, 5)
FactoryBot.create(:organisation, name: "Acme LTD")
end
let(:organisation_list) { Organisation.all }
context "when given a search term" do
let(:search_term) { "Acme" }
let(:organisation_list) { Organisation.all }
it "filters the collection on search term" do
expect(described_class.filter_by_search(organisation_list, search_term).count).to eq(1)
end
end
context "when given a search term" do
let(:search_term) { "Acme" }
context "when not given a search term" do
let(:search_term) { nil }
it "filters the collection on search term" do
expect(described_class.filter_by_search(organisation_list, search_term).count).to eq(1)
it "does not filter the given collection" do
expect(described_class.filter_by_search(organisation_list, search_term).count).to eq(6)
end
end
end
context "when not given a search term" do
let(:search_term) { nil }
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(organisation_list, search_term).count).to eq(6)
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

318
spec/services/imports/sales_logs_import_service_spec.rb

@ -17,14 +17,6 @@ RSpec.describe Imports::SalesLogsImportService do
end
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).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)
@ -119,6 +111,29 @@ RSpec.describe Imports::SalesLogsImportService do
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
let(:sales_log_id) { "discounted_ownership_sales_log" }
@ -188,6 +203,228 @@ RSpec.describe Imports::SalesLogsImportService do
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 the armedforcesspouse is not answered" do
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&.status).to eq("completed")
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
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)
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

16
spec/support/bulk_upload/log_to_csv.rb

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

Loading…
Cancel
Save