Browse Source

Merge 0d900060d8 into 7b5325c8bd

pull/3134/merge
Samuel Young 2 weeks ago committed by GitHub
parent
commit
97d7a5efd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      app/components/create_log_actions_component.html.erb
  2. 10
      app/controllers/test_data_controller.rb
  3. 11
      app/helpers/bulk_upload/lettings_log_to_csv.rb
  4. 9
      app/helpers/bulk_upload/sales_log_to_csv.rb
  5. 2
      app/models/bulk_upload.rb
  6. 2
      app/services/bulk_upload/lettings/log_creator.rb
  7. 2
      app/services/bulk_upload/lettings/validator.rb
  8. 122
      app/services/bulk_upload/lettings/year2026/csv_parser.rb
  9. 1704
      app/services/bulk_upload/lettings/year2026/row_parser.rb
  10. 2
      app/services/bulk_upload/sales/log_creator.rb
  11. 2
      app/services/bulk_upload/sales/validator.rb
  12. 124
      app/services/bulk_upload/sales/year2026/csv_parser.rb
  13. 1499
      app/services/bulk_upload/sales/year2026/row_parser.rb
  14. 4
      config/routes.rb
  15. 87
      spec/fixtures/files/2026_27_lettings_bulk_upload.csv
  16. 23
      spec/fixtures/files/2026_27_sales_bulk_upload.csv
  17. 7
      spec/models/bulk_upload_spec.rb
  18. 12
      spec/services/bulk_upload/lettings/log_creator_spec.rb
  19. 36
      spec/services/bulk_upload/lettings/validator_spec.rb
  20. 254
      spec/services/bulk_upload/lettings/year2026/csv_parser_spec.rb
  21. 2882
      spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb
  22. 316
      spec/services/bulk_upload/sales/log_creator_spec.rb
  23. 50
      spec/services/bulk_upload/sales/validator_spec.rb
  24. 191
      spec/services/bulk_upload/sales/year2026/csv_parser_spec.rb
  25. 1981
      spec/services/bulk_upload/sales/year2026/row_parser_spec.rb

8
app/components/create_log_actions_component.html.erb

@ -25,15 +25,15 @@
<path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"></path>
</svg>
<% end %>
<%= govuk_button_link_to create_test_bulk_upload_href(2024), class: "govuk-button govuk-button--secondary" do %>
24/25 BU test file
<%= govuk_button_link_to create_test_bulk_upload_href(2025), class: "govuk-button govuk-button--secondary" do %>
25/26 BU test file
<svg class="govuk-button__start-icon bi bi-download" xmlns="http://www.w3.org/2000/svg" width="18" height="19" fill="currentColor" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" />
</svg>
<% end %>
<%= govuk_button_link_to create_test_bulk_upload_href(2025), class: "govuk-button govuk-button--secondary" do %>
25/26 BU test file
<%= govuk_button_link_to create_test_bulk_upload_href(2026), class: "govuk-button govuk-button--secondary" do %>
26/27 BU test file
<svg class="govuk-button__start-icon bi bi-download" xmlns="http://www.w3.org/2000/svg" width="18" height="19" fill="currentColor" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.4">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" />

10
app/controllers/test_data_controller.rb

@ -17,12 +17,13 @@ class TestDataController < ApplicationController
redirect_to lettings_log_path(log)
end
%w[2024 2025].each do |year|
%w[2025 2026].each do |year|
define_method("create_#{year}_test_lettings_bulk_upload") do
return render_not_found unless FeatureToggle.create_test_logs_enabled?
file = Tempfile.new("#{year}_test_lettings_log.csv")
log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA", startdate: generate_different_date_within_collection_year(Time.zone.local(year.to_i, 4, 1), end_date_override: Time.zone.now + 14.days))
end_date_override = year == current_collection_start_year ? Time.zone.now + 14.days : collection_start_date(Time.zone.local(year.to_i, 4, 1)) + 14.days
log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA", startdate: generate_different_date_within_collection_year(Time.zone.local(year.to_i, 4, 1), end_date_override:))
log_to_csv = BulkUpload::LettingsLogToCsv.new(log:, line_ending: "\n", overrides: { organisation_id: "ORG#{log.owning_organisation_id}", managing_organisation_id: "ORG#{log.owning_organisation_id}" })
file.write(log_to_csv.default_field_numbers_row)
file.write(log_to_csv.to_csv_row)
@ -53,12 +54,13 @@ class TestDataController < ApplicationController
redirect_to sales_log_path(log)
end
%w[2024 2025].each do |year|
%w[2025 2026].each do |year|
define_method("create_#{year}_test_sales_bulk_upload") do
return render_not_found unless FeatureToggle.create_test_logs_enabled?
file = Tempfile.new("#{year}_test_sales_log.csv")
log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, value: 180_000, deposit: 150_000, county: "Somerset", saledate: generate_different_date_within_collection_year(Time.zone.local(year.to_i, 4, 1), end_date_override: Time.zone.now + 14.days))
end_date_override = year == current_collection_start_year ? Time.zone.now + 14.days : collection_start_date(Time.zone.local(year.to_i, 4, 1)) + 14.days
log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, value: 180_000, deposit: 150_000, county: "Somerset", saledate: generate_different_date_within_collection_year(Time.zone.local(year.to_i, 4, 1), end_date_override:))
log_to_csv = BulkUpload::SalesLogToCsv.new(log:, line_ending: "\n", overrides: { organisation_id: "ORG#{log.owning_organisation_id}", managing_organisation_id: "ORG#{log.owning_organisation_id}" })
file.write(log_to_csv.default_field_numbers_row)
file.write(log_to_csv.to_csv_row)

11
app/helpers/bulk_upload/lettings_log_to_csv.rb

@ -17,7 +17,7 @@ class BulkUpload::LettingsLogToCsv
def to_csv_row(seed: nil)
year = log.collection_start_year
case year
when 2022, 2023, 2024, 2025
when 2022, 2023, 2024, 2025, 2026
to_year_csv_row(year, seed:)
else
raise NotImplementedError "No mapping function implemented for year #{year}"
@ -91,6 +91,15 @@ class BulkUpload::LettingsLogToCsv
(1..129).to_a
end
def default_2026_field_numbers
(1..129).to_a
end
def to_2026_row
# TODO: Implement when 2026 format is known
to_2025_row
end
def to_2025_row
[
overrides[:organisation_id] || log.owning_organisation&.old_visible_id, # 1

9
app/helpers/bulk_upload/sales_log_to_csv.rb

@ -19,7 +19,7 @@ class BulkUpload::SalesLogToCsv
case year
when 2022
to_2022_csv_row
when 2023, 2024, 2025
when 2023, 2024, 2025, 2026
to_year_csv_row(year, seed:)
else
raise NotImplementedError "No mapping function implemented for year #{year}"
@ -69,6 +69,8 @@ class BulkUpload::SalesLogToCsv
(1..131).to_a
when 2025
(1..121).to_a
when 2026
(1..121).to_a
else
raise NotImplementedError "No mapping function implemented for year #{year}"
end
@ -532,6 +534,11 @@ class BulkUpload::SalesLogToCsv
]
end
def to_2026_row
# TODO: Implement when 2026 template is available
to_2025_row
end
def custom_field_numbers_row(seed: nil, field_numbers: nil)
if seed
["Field number"] + field_numbers.shuffle(random: Random.new(seed))

2
app/models/bulk_upload.rb

@ -104,6 +104,8 @@ class BulkUpload < ApplicationRecord
end
year_class = case year
when 2026
"Year2026"
when 2025
"Year2025"
when 2024

2
app/services/bulk_upload/lettings/log_creator.rb

@ -36,6 +36,8 @@ private
BulkUpload::Lettings::Year2024::CsvParser.new(path:)
when 2025
BulkUpload::Lettings::Year2025::CsvParser.new(path:)
when 2026
BulkUpload::Lettings::Year2026::CsvParser.new(path:)
else
raise "csv parser not found"
end

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

@ -115,6 +115,8 @@ private
BulkUpload::Lettings::Year2024::CsvParser.new(path:)
when 2025
BulkUpload::Lettings::Year2025::CsvParser.new(path:)
when 2026
BulkUpload::Lettings::Year2026::CsvParser.new(path:)
else
raise "csv parser not found"
end

122
app/services/bulk_upload/lettings/year2026/csv_parser.rb

@ -0,0 +1,122 @@
require "csv"
class BulkUpload::Lettings::Year2026::CsvParser
include CollectionTimeHelper
FIELDS = 129
MAX_COLUMNS = 130
FORM_YEAR = 2026
attr_reader :path
def initialize(path:)
@path = path
end
def row_offset
if with_headers?
rows.find_index { |row| row[0].present? && row[0].match(/field number/i) } + 1
else
0
end
end
def col_offset
with_headers? ? 1 : 0
end
def cols
@cols ||= ("A".."DZ").to_a
end
def row_parsers
@row_parsers ||= body_rows.map { |row|
next if row.empty?
stripped_row = row[col_offset..]
hash = Hash[field_numbers.zip(stripped_row)]
BulkUpload::Lettings::Year2026::RowParser.new(hash)
}.compact
end
def body_rows
rows[row_offset..]
end
def rows
@rows ||= CSV.parse(normalised_string, row_sep:)
end
def column_for_field(field)
cols[field_numbers.find_index(field) + col_offset]
end
def correct_field_count?
valid_field_numbers_count = field_numbers.count { |f| f != "field_blank" }
valid_field_numbers_count == FIELDS
end
def too_many_columns?
return if with_headers?
max_columns_count = body_rows.map(&:size).max - col_offset
max_columns_count > MAX_COLUMNS
end
def wrong_template_for_year?
collection_start_year_for_date(first_record_start_date) != FORM_YEAR
rescue Date::Error
false
end
def missing_required_headers?
!with_headers?
end
private
def default_field_numbers
(1..FIELDS).map { |h| h.present? && h.to_s.match?(/^[0-9]+$/) ? "field_#{h}" : "field_blank" }
end
def field_numbers
@field_numbers ||= if with_headers?
rows[row_offset - 1][col_offset..].map { |h| h.present? && h.match?(/^[0-9]+$/) ? "field_#{h}" : "field_blank" }
else
default_field_numbers
end
end
def with_headers?
rows.map { |r| r[0] }.any? { |cell| cell&.match?(/field number/i) }
end
def row_sep
"\n"
end
def normalised_string
return @normalised_string if @normalised_string
@normalised_string = File.read(path, encoding: "bom|utf-8")
@normalised_string.gsub!("\r\n", "\n")
@normalised_string.scrub!("")
@normalised_string.tr!("\r", "\n")
@normalised_string
end
def first_record_start_date
if with_headers?
year = row_parsers.first.field_10.to_s.strip.length.between?(1, 2) ? row_parsers.first.field_10.to_i + 2000 : row_parsers.first.field_10.to_i
Date.new(year, row_parsers.first.field_9.to_i, row_parsers.first.field_8.to_i)
else
year = rows.first[9].to_s.strip.length.between?(1, 2) ? rows.first[9].to_i + 2000 : rows.first[9].to_i
Date.new(year, rows.first[8].to_i, rows.first[7].to_i)
end
end
end

1704
app/services/bulk_upload/lettings/year2026/row_parser.rb

File diff suppressed because it is too large Load Diff

2
app/services/bulk_upload/sales/log_creator.rb

@ -35,6 +35,8 @@ private
BulkUpload::Sales::Year2024::CsvParser.new(path:)
when 2025
BulkUpload::Sales::Year2025::CsvParser.new(path:)
when 2026
BulkUpload::Sales::Year2026::CsvParser.new(path:)
else
raise "csv parser not found"
end

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

@ -111,6 +111,8 @@ private
BulkUpload::Sales::Year2024::CsvParser.new(path:)
when 2025
BulkUpload::Sales::Year2025::CsvParser.new(path:)
when 2026
BulkUpload::Sales::Year2026::CsvParser.new(path:)
else
raise "csv parser not found"
end

124
app/services/bulk_upload/sales/year2026/csv_parser.rb

@ -0,0 +1,124 @@
require "csv"
class BulkUpload::Sales::Year2026::CsvParser
include CollectionTimeHelper
FIELDS = 121
MAX_COLUMNS = 142
FORM_YEAR = 2026
attr_reader :path
def initialize(path:)
@path = path
end
def row_offset
if with_headers?
rows.find_index { |row| row[0].present? && row[0].match(/field number/i) } + 1
else
0
end
end
def col_offset
with_headers? ? 1 : 0
end
def cols
@cols ||= ("A".."DR").to_a
end
def row_parsers
@row_parsers ||= body_rows.map { |row|
next if row.empty?
stripped_row = row[col_offset..]
hash = Hash[field_numbers.zip(stripped_row)]
BulkUpload::Sales::Year2026::RowParser.new(hash)
}.compact
end
def body_rows
rows[row_offset..]
end
def rows
@rows ||= CSV.parse(normalised_string, row_sep:)
end
def column_for_field(field)
cols[field_numbers.find_index(field) + col_offset]
end
def wrong_template_for_year?
collection_start_year_for_date(first_record_start_date) != FORM_YEAR
rescue Date::Error
false
end
def missing_required_headers?
!with_headers?
end
def correct_field_count?
valid_field_numbers_count = field_numbers.count { |f| f != "field_blank" }
valid_field_numbers_count == FIELDS
end
private
def default_field_numbers
(1..FIELDS).map do |number|
if number.to_s.match?(/^[0-9]+$/)
"field_#{number}"
else
"field_blank"
end
end
end
def field_numbers
@field_numbers ||= if with_headers?
rows[row_offset - 1][col_offset..].map { |number| number.to_s.match?(/^[0-9]+$/) ? "field_#{number}" : "field_blank" }
else
default_field_numbers
end
end
def headers
@headers ||= ("field_1".."field_#{FIELDS}").to_a
end
def with_headers?
# we will eventually want to validate that headers exist for this year
rows.map { |r| r[0] }.any? { |cell| cell&.match?(/field number/i) }
end
def row_sep
"\n"
end
def normalised_string
return @normalised_string if @normalised_string
@normalised_string = File.read(path, encoding: "bom|utf-8")
@normalised_string.gsub!("\r\n", "\n")
@normalised_string.scrub!("")
@normalised_string.tr!("\r", "\n")
@normalised_string
end
def first_record_start_date
if with_headers?
year = row_parsers.first.field_3.to_s.strip.length.between?(1, 2) ? row_parsers.first.field_3.to_i + 2000 : row_parsers.first.field_3.to_i
Date.new(year, row_parsers.first.field_2.to_i, row_parsers.first.field_1.to_i)
else
year = rows.first[2].to_s.strip.length.between?(1, 2) ? rows.first[2].to_i + 2000 : rows.first[2].to_i
Date.new(year, rows.first[1].to_i, rows.first[0].to_i)
end
end
end

1499
app/services/bulk_upload/sales/year2026/row_parser.rb

File diff suppressed because it is too large Load Diff

4
config/routes.rb

@ -406,11 +406,11 @@ Rails.application.routes.draw do
if FeatureToggle.create_test_logs_enabled?
get "create-test-lettings-log", to: "test_data#create_test_lettings_log"
get "create-setup-test-lettings-log", to: "test_data#create_setup_test_lettings_log"
get "create-2024-test-lettings-bulk-upload", to: "test_data#create_2024_test_lettings_bulk_upload"
get "create-2025-test-lettings-bulk-upload", to: "test_data#create_2025_test_lettings_bulk_upload"
get "create-2026-test-lettings-bulk-upload", to: "test_data#create_2026_test_lettings_bulk_upload"
get "create-test-sales-log", to: "test_data#create_test_sales_log"
get "create-setup-test-sales-log", to: "test_data#create_setup_test_sales_log"
get "create-2024-test-sales-bulk-upload", to: "test_data#create_2024_test_sales_bulk_upload"
get "create-2025-test-sales-bulk-upload", to: "test_data#create_2025_test_sales_bulk_upload"
get "create-2026-test-sales-bulk-upload", to: "test_data#create_2026_test_sales_bulk_upload"
end
end

87
spec/fixtures/files/2026_27_lettings_bulk_upload.csv vendored

@ -0,0 +1,87 @@
Section,Setting up this lettings log,,,,,,,,,,,,,,,Property information,,,,,,,,,,,,,,,,,,,,,Tenancy information,,,,,Household characteristics,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,Household needs,,,,,,,,,,,,,,,,,,,,,Household situation,,,,,,,,,,,,,,,,,,,,,"Income, benefits and outgoings",,,,,,,,,,,,
Question,Which organisation owns this property?,Which organisation manages this letting?,What is the CORE username of the account this letting log should be assigned to? ,What is the needs type?,What scheme does this letting belong to?,Which location is this letting for?,Is this letting a renewal of social housing to the same tenant in the same property?,What is the tenancy start date? - day DD,What is the tenancy start date? - month MM,What is the tenancy start date? - year YY,What is the rent type?,Which 'Other' type of Intermediate Rent is this letting?,What is the tenant code?,What is the property reference?,Has the tenant seen or been given access to the MHCLG privacy notice?,What is the reason for the property being vacant?,What type was the property most recently let as?,"If known, provide this property’s UPRN",Address Line 1,Address Line 2,Town or city,County,Part 1 of the property's postcode,Part 2 of the property's postcode,What is the property's local authority?,What type of unit is the property?,Which type of building is the property?,Is the property built or adapted to wheelchair-user standards?,How many bedrooms does the property have?,What is the void date? - day DD,What is the void date? - month MM,What is the void date? - year YY,What date were any major repairs completed on? - day DD,What date were any major repairs completed on? - month MM,What date were any major repairs completed on? - year YY,Is this property older people's housing?,Is this a joint tenancy?,Is this a starter tenancy?,What is the type of tenancy?,"If 'Other', what is the type of tenancy?",What is the length of the fixed-term tenancy to the nearest year?,What is the lead tenant’s age?,Which of these best describes the lead tenant’s gender identity? ,Which of these best describes the lead tenant's ethnic background?,What is the lead tenant’s nationality?,Which of these best describes the lead tenant’s working situation?,Is person 2 the partner of the lead tenant?,What is person 2's age?,Which of these best describes person 2's gender identity?,Which of these best describes person 2's working situation?,Is person 3 the partner of the lead tenant?,What is person 3's age?,Which of these best describes person 3's gender identity?,Which of these best describes person 3's working situation?,Is person 4 the partner of the lead tenant?,What is person 4's age?,Which of these best describes person 4's gender identity?,Which of these best describes person 4's working situation?,Is person 5 the partner of the lead tenant?,What is person 5's age?,Which of these best describes person 5's gender identity?,Which of these best describes person 5's working situation?,Is person 6 the partner of the lead tenant?,What is person 6's age?,Which of these best describes person 6's gender identity?,Which of these best describes person 6's working situation?,Is person 7 the partner of the lead tenant?,What is person 7's age?,Which of these best describes person 7's gender identity?,Which of these best describes person 7's working situation?,Is person 8 the partner of the lead tenant?,What is person 8's age?,Which of these best describes person 8's gender identity?,Which of these best describes person 8's working situation?,Does anybody in the household have links to the UK armed forces?,Is this person still serving in the UK armed forces?,Was this person seriously injured or ill as a result of serving in the UK armed forces?,Is anybody in the household pregnant?,"Disabled access needs
a) Fully wheelchair-accessible housing","Disabled access needs
b) Wheelchair access to essential rooms","Disabled access needs
c) Level access housing","Disabled access needs
f) Other disabled access needs","Disabled access needs
g) No disabled access needs","Disabled access needs
h) Don’t know",Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more?,Does this person's condition affect their dexterity?,Does this person's condition affect their learning or understanding or concentrating?,Does this person's condition affect their hearing?,Does this person's condition affect their memory?,Does this person's condition affect their mental health?,Does this person's condition affect their mobility?,Does this person's condition affect them socially or behaviourally?,Does this person's condition affect their stamina or breathing or fatigue?,Does this person's condition affect their vision?,Does this person's condition affect them in another way?,How long has the household continuously lived in the local authority area of the new letting?,How long has the household been on the local authority housing register (or waiting list) for the area of the new letting?,What is the tenant’s main reason for the household leaving their last settled home?,"If 'Other', what was the main reason for leaving their last settled home?",Where was the household immediately before this letting?,Did the household experience homelessness immediately before this letting?,Do you know the postcode of the household's last settled home?,Part 1 of postcode of last settled home,Part 2 of postcode of last settled home,What is the local authority of the household's last settled home?,Was the household given 'reasonable preference' by the local authority?,"Reasonable preference reason
They were homeless or about to lose their home (within 56 days)","Reasonable preference reason
They were living in unsanitary, overcrowded or unsatisfactory housing","Reasonable preference reason
They needed to move due to medical and welfare reasons (including disability)","Reasonable preference reason
They needed to move to avoid hardship to themselves or others","Reasonable preference reason
Don't know","How was this letting allocated?
Choice based Lettings (CBL)","How was this letting allocated?
Common Allocations Policy (CAP)","How was this letting allocated?
Common Housing Register (CHR)","How was this letting allocated?
Accessible Housing Register",What was the source of referral for this letting?,Do you know the household's combined total income after tax?,How often does the household receive income?,How much income does the household have in total?,Is the tenant likely to be receiving any of these housing-related benefits?,"How much of the household's income is from Universal Credit, state pensions or benefits?",Does the household pay rent or other charges for the accommodation?,How often does the household pay rent and other charges?,What is the basic rent?,What is the service charge?,What is the personal service charge?,What is the support charge?,"After the household has received any housing-related benefits, will they still need to pay for rent and charges?",What do you expect the outstanding amount to be?
Additional info,"You can find the org ID on the CORE service under 'Stock owners' or, if your organisation is the stock owner, under 'About your organisation'","You can find the org ID on the CORE service under 'Managing agents' or, if your organisation is the managing agent, under 'About your organisation'","If left empty, the letting log will be assigned to the account used to upload the log.","General needs housing includes both self-contained and shared housing without support or specific adaptations. Supported housing includes direct access hostels, group homes, residential care and nursing homes.","Scheme code. Include the 'S' at the beginning if it has one.
You can find the scheme code on the CORE service under 'Schemes', either by searching for the specific scheme or downloading a csv.","Location code.
You can find the location code on the CORE service under 'Schemes', either by searching for the specific location or downloading a csv.","If the property was previously being used as temporary accommodation, then answer 'no'.",,,,See specification for definitions,,This is how you usually refer to this tenancy on your own systems.,This is how you usually refer to this property on your own systems.,"Make sure the lead tenant has seen or been given access to the Ministry of Housing, Communities and Local Government (MHCLG) privacy notice before completing this log. This is a legal requirement under data protection legislation.","Internal transfer - Where a tenant moved from one social housing property to another property. Their landlord may be the same or may have changed.
Renewal of a fixed term tenancy - to the same tenant in the same property, except if was previously used as temporary accommodation.",This is the rent type of the previous tenancy in this property.,"The Unique Property Reference Number (UPRN) is a unique number system created by Ordnance Survey and used by housing providers and various industries across the UK. An example UPRN is 10010457355.
The UPRN may not be the same as the property reference assigned by your organisation.",,,,,Combined with field 22 it should be a postcode which lies within the local authority given in field 25.,Combined with field 21 it should be a postcode which lies within the local authority given in field 25.,,,,"This is whether someone who uses a wheelchair is able to make full use of all of the property’s rooms and facilities, including use of both inside and outside space, and entering and exiting the property.","If shared accommodation, enter the number of bedrooms occupied by this household. A bedsit has 1 bedroom.","Date the property was (legally/contractually) available to let, or for:
- re-lets: the day after previous tenant’s contract end
- new-builds: the day the landlord legally owned the property ('completion date’)
- new conversions or acquisitions: the completion date, or the day after any rehabilitation work ended
- new leases: the day the landlord got contractual property rights, and could let it out to tenants.",,,"Major repairs are works that could not be reasonably carried out with a tenant living at the property. For example, structural repairs.",,,"This includes retirement living, sheltered housing and extra care housing. There is no national set limit for “older people”, please answer based on your own policies.
Extra care housing is for tenants with medium to high care and support needs, often with 24 hour access to support staff provided by an agency registered with the Care Quality Commission.",This is where two or more people are named on the tenancy agreement.,"If the tenancy has an ‘introductory period’ answer ‘yes’.
You should submit a CORE log at the beginning of the starter tenancy or introductory period, with the best information you have at the time. You do not need to submit a log when a tenant later rolls onto the main tenancy.",This is about the main tenancy after any starter or introductory period. See specification for definitions.,,Do not include the starter or introductory period. The minimum period is 2 years for social or affordable rent general needs logs. You do not need to submit CORE logs for these types of tenancies if they are shorter than 2 years.,"This is the household member who does the most paid work. If several people do the same amount of paid work, it's the oldest household member.",This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth.,,"If the lead tenant is a dual national of the United Kingdom and another country, enter United Kingdom. If they are a dual national of two other countries, the tenant should decide which country to enter.","This is the household member who does the most paid work. If several people do the same amount of paid work, it's the oldest household member.",,Answer 1 for children aged under 1 year old,This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth.,,,Answer 1 for children aged under 1 year old,This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth.,,,Answer 1 for children aged under 1 year old,This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth.,,,Answer 1 for children aged under 1 year old,This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth.,,,Answer 1 for children aged under 1 year old,This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth.,,,Answer 1 for children aged under 1 year old,This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth.,,,Answer 1 for children aged under 1 year old,This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth.,,"This excludes national service.
If several household members have these links, answer for regular first. If no regular, answer for reserve. If no reserve, answer for spouses or civil partners.",,,,,,,,,,,"For example, lifting and carrying objects, or using a keyboard",,"For example, deafness or partial hearing",,"For example, depression or anxiety","For example, walking short distances or climbing stairs","For example, anything associated with autism spectrum disorder (ASD), including Asperger’s or attention deficit hyperactivity disorder (ADHD)",,"For example, blindness or partial sight",,,,"The tenant's ‘last settled home' is their last long-standing home. For tenants who had temporary accommodation, sleeping rough or otherwise homeless, their last settled home is where they were living previously.",,,,"This is the tenant’s last long-standing home. It is where the tenant was living before any period in temporary accommodation, sleeping rough or otherwise homeless.","Combined with field 104, it should be a postcode which lies within the local authority given in field 105.","Combined with field 103, it should be a postcode which lies within the local authority given in field 105.","This is the tenant’s last long-standing home. It is where the tenant was living before any period in temporary accommodation, sleeping rough or otherwise homeless.",Households may be given ‘reasonable preference’ for social housing under one or more specific category by the local authority. This is also known as ‘priority need’.,,,,,,Where available vacant properties are advertised and applicants are able to bid for specific properties.,Where a common system agreed between a group of housing providers is used to determine applicants' priority for housing.,Where a single waiting list is used by a group of housing providers to receive and process housing applications. Providers may use different approaches to determine priority.,Where the 'access category' or another descriptor of whether an available vacant property meets a range of access needs is displayed to applicants during the allocations process.,,,,"Include any income after tax from employment, pensions, and Universal Credit. Don't include National Insurance (NI) contributions and tax, housing benefit, child benefit, or council tax support.","This is about when the tenant is in their new let. If they are unsure about the situation for their new let and their financial and working situation hasn’t changed significantly, answer based on what housing-related benefits they currently receive.",,"If rent is charged on the property then answer Yes, even if tenants do not pay it themselves.",,"This is the amount paid before any charges are added for services (for example, hot water or cleaning). Households may receive housing benefit or Universal Credit towards basic rent.","For example, cleaning. Households may get household benefits towards the service charge.",For example heating or hot water. This doesn’t include housing benefit or Universal Credit.,Any charges made to fund support services included in the tenancy agreement.,Also known as the 'outstanding amount',You only need to give an approximate figure.
Values,Alphanumeric,,Email format,01-Feb,Alphanumeric,Numeric,01-Feb,Jan-31,01-Dec,25 - 26,01-Jul,Text,"Alphanumeric, max 13 characters","Alphanumeric, max 12 characters",1,"5 - 6, or 8 - 22",1 - 3 or 5 - 9,Numeric,Alphanumeric,,Text,,"Alphanumeric,
2 - 4 characters","Alphanumeric,
3 characters","9 character ONS code, beginning with 'E' (https://www.get-information-schools.service.gov.uk/Guidance/LaNameCodes) ","1 - 2, 4 or 6 - 10",01-Feb,01-Feb,01-Jul,Jan-31,01-Dec,Jun-26,Jan-31,01-Dec,Jun-26,"2 - 4, 7 - 8",01-Mar,01-Feb,02-Aug,Text,"1 - 99, see specification for more detail",16 - 120 or R,"F, M, X or R",Jan-20,"3 digit ISO country code, see specification",0 - 10,01-Mar,"Numeric, range 1 - 120 or text (upper case 'R')
Must be >= 16 if working situation = 1 - 8 or 0
Must be <16 if working situation = 9","F, M, X or R","0 - 10
Must be 9 if age <16",01-Mar,"Numeric, range 1 - 120 or text (upper case 'R')
Must be >= 16 if working situation = 1 - 8 or 0
Must be <16 if working situation = 9","F, M, X or R","0 - 10
Must be 9 if age <16",01-Mar,"Numeric, range 1 - 120 or text (upper case 'R')
Must be >= 16 if working situation = 1 - 8 or 0
Must be <16 if working situation = 9","F, M, X or R","0 - 10
Must be 9 if age <16",01-Mar,"Numeric, range 1 - 120 or text (upper case 'R')
Must be >= 16 if working situation = 1 - 8 or 0
Must be <16 if working situation = 9","F, M, X or R","0 - 10
Must be 9 if age <16",01-Mar,"Numeric, range 1 - 120 or text (upper case 'R')
Must be >= 16 if working situation = 1 - 8 or 0
Must be <16 if working situation = 9","F, M, X or R","0 - 10
Must be 9 if age <16",01-Mar,"Numeric, range 1 - 120 or text (upper case 'R')
Must be >= 16 if working situation = 1 - 8 or 0
Must be <16 if working situation = 9","F, M, X or R","0 - 10
Must be 9 if age <16",01-Mar,"Numeric, range 1 - 120 or text (upper case 'R')
Must be >= 16 if working situation = 1 - 8 or 0
Must be <16 if working situation = 9","F, M, X or R","0 - 10
Must be 9 if age <16",01-Jun,03-Jun,01-Mar,,1 or empty,,,,,,01-Mar,1 or empty,,,,,,,,,,1 - 2 or 6 - 12,2 or 6 - 13,"1 - 2, 4, 8 - 14, 16 - 20, 28 - 31, 34 or 44 - 55",Text,"3 - 4, 6 - 7, 9 - 10, 13 - 14, 18 - 19, 21, 23 - 33, 35, 37 - 39 ",1 or 11,01-Feb,"Alphanumeric, 2 - 4 characters","Alphanumeric,
3 characters","9 character ONS code, beginning with 'E' (https://www.get-information-schools.service.gov.uk/Guidance/LaNameCodes) ",01-Mar,1 or empty,,,,,01-Feb,,,,"1 - 4, 7 - 10, 14 - 24",01-Mar,01-Mar,0 - 99999,"1, 3, 6, 9 or 10",01-Apr,0 - 1,01-Oct,xxxx.xx,,,,01-Mar,xxxx.xx
Can be empty?,No,,Yes,No,"Yes, if letting is general needs (if field 4 = 1)","Yes, if letting is general needs (if field 4 = 1)",No,,,,,"Yes, if letting is not 'Other intermediate rent product' (if field 11 is not 6)",Yes,,No,"Yes, if letting is a renewal (if field 7 = 1)","Yes, if letting is a renewal (if field 7 = 1) or a first-time let (if field 16 = 15 - 17)","Yes, if letting is supported housing (if field 4 = 2) or if the property's postcode is not empty (if fields 23 and 24 contain full and valid entries)","Yes, if letting is supported housing (if field 4 = 2) or if property's UPRN and local authority are known (if fields 18 and 25 are not empty)",Yes,"Yes, if letting is supported housing (if field 4 = 2) or if property's UPRN and local authority are known (if fields 18 and 25 are not empty)",Yes,"Yes, if letting is supported housing (if field 4 = 2) or if property's UPRN and local authority are known (if fields 18 and 25 are not empty)",,"Yes, if letting is supported housing (if field 4 = 2)",,,,,"Yes, if letting is a renewal (if field 7 = 1)",,,Yes,,,"Yes, if letting is general needs (if field 4 = 1)",No,,,"Yes, if 'Other' is not selected for tenancy type (if field 39 is not 3)","Yes, if letting is not a fixed-term tenancy (if field 39 is not 4 or 6)",No,,,,,"Yes, if all fields about person 2 are empty (fields 47 - 50)",,,,"Yes, if all fields about person 3 are empty (fields 51 - 54)",,,,"Yes, if all fields about person 4 are empty (fields 55 - 58)",,,,"Yes, if all fields about person 5 are empty (fields 59 - 62)",,,,"Yes, if all fields about person 6 are empty (fields 63 - 66)",,,,"Yes, if all fields about person 7 are empty (fields 67 - 70)",,,,"Yes, if all fields about person 8 are empty (fields 71 - 74)",,,,No,"Yes, if no one in the household is a current or former regular (if field 75 is not 1)","Yes, if no one in the household is a current or former regular or reserve (if field 75 is not 1 or 4)",No,"Yes, if no household members have access needs or if it is unknown (if field 83 or 84 = 1)",,,,"Yes, if a household member has an access need (if at least one of fields 79 to 82 = 1)",,No,"Yes, if a household member has an access need (if at least one of fields 79 to 82 = 1)
If someone in the household does have such a condition (if field 89 = 1), then at least 1 of these fields must be 1.",,,,,,,,,,No,"Yes, if letting is a renewal (if field 7 = 1)",No,"Yes, if 'Other' is not selected for reason for leaving last settled home (if field 98 is not 20)",No,No,,"Yes, if postcode of household's last settled home is not known (if 102 = 2)",,Yes,No,"If household was given 'reasonable preference' (if field 107 = 1), at least one of these fields must be 1
If household was not given 'reasonable preference' (if field 106 = 2 or 3), these fields will be ignored.",,,,,No,,,,"Yes, if letting is a renewal (if field 7 = 1)",No,"Yes, if household's income is unknown (if field 117 = 2 or 3)",,No,,"Yes, if letting is supported housing (if field 4 = 2)",No,"Yes, if the household does not pay rent (if field 122 = 1)",,,,"Yes, if the household doesn't receive housing benefits, or if it is unknown (if field 120 = 3, 9 or 10)","Yes, if the household does not need to pay rent or charges after receiving housing benefits (if field 128 is not 1)"
Type of letting the question applies to,,,,,Supported housing only,,,,,,,Other Intermediate Rent only,,,,,,General needs only,,,,,,,,,,,,,,,,,,Supported housing only,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,Supported housing only,,,,,,,
Duplicate check field?,Yes,,,,Yes,,,Yes,,,,,Yes,,,,,,,,,,Yes,,,,,,,,,,,,,,,,,,,Yes,,,,Yes,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
Field number,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129
,ORG1,ORG1,support@example.com,1,,,2,1,4,26,1,,1,1,1,5,1,,a,a,a,a,a1,1aa,E09000001,1,1,1,1,1,4,25,,,,,3,1,2,,,20,F,1,GBR,1,,,,,,,,,,,,,,,,,,,,,,,,,,,,,1,6,3,1,1,,,,,,1,1,,,,,,,,,,1,2,50,,30,1,2,,,,1,1,,,,,1,1,1,1,7,2,,,1,2,1,1,50,0,0,0,3,
1 Section Setting up this lettings log Property information Tenancy information Household characteristics Household needs Household situation Income, benefits and outgoings
2 Question Which organisation owns this property? Which organisation manages this letting? What is the CORE username of the account this letting log should be assigned to? What is the needs type? What scheme does this letting belong to? Which location is this letting for? Is this letting a renewal of social housing to the same tenant in the same property? What is the tenancy start date? - day DD What is the tenancy start date? - month MM What is the tenancy start date? - year YY What is the rent type? Which 'Other' type of Intermediate Rent is this letting? What is the tenant code? What is the property reference? Has the tenant seen or been given access to the MHCLG privacy notice? What is the reason for the property being vacant? What type was the property most recently let as? If known, provide this property’s UPRN Address Line 1 Address Line 2 Town or city County Part 1 of the property's postcode Part 2 of the property's postcode What is the property's local authority? What type of unit is the property? Which type of building is the property? Is the property built or adapted to wheelchair-user standards? How many bedrooms does the property have? What is the void date? - day DD What is the void date? - month MM What is the void date? - year YY What date were any major repairs completed on? - day DD What date were any major repairs completed on? - month MM What date were any major repairs completed on? - year YY Is this property older people's housing? Is this a joint tenancy? Is this a starter tenancy? What is the type of tenancy? If 'Other', what is the type of tenancy? What is the length of the fixed-term tenancy to the nearest year? What is the lead tenant’s age? Which of these best describes the lead tenant’s gender identity? Which of these best describes the lead tenant's ethnic background? What is the lead tenant’s nationality? Which of these best describes the lead tenant’s working situation? Is person 2 the partner of the lead tenant? What is person 2's age? Which of these best describes person 2's gender identity? Which of these best describes person 2's working situation? Is person 3 the partner of the lead tenant? What is person 3's age? Which of these best describes person 3's gender identity? Which of these best describes person 3's working situation? Is person 4 the partner of the lead tenant? What is person 4's age? Which of these best describes person 4's gender identity? Which of these best describes person 4's working situation? Is person 5 the partner of the lead tenant? What is person 5's age? Which of these best describes person 5's gender identity? Which of these best describes person 5's working situation? Is person 6 the partner of the lead tenant? What is person 6's age? Which of these best describes person 6's gender identity? Which of these best describes person 6's working situation? Is person 7 the partner of the lead tenant? What is person 7's age? Which of these best describes person 7's gender identity? Which of these best describes person 7's working situation? Is person 8 the partner of the lead tenant? What is person 8's age? Which of these best describes person 8's gender identity? Which of these best describes person 8's working situation? Does anybody in the household have links to the UK armed forces? Is this person still serving in the UK armed forces? Was this person seriously injured or ill as a result of serving in the UK armed forces? Is anybody in the household pregnant? Disabled access needs a) Fully wheelchair-accessible housing Disabled access needs b) Wheelchair access to essential rooms Disabled access needs c) Level access housing Disabled access needs f) Other disabled access needs Disabled access needs g) No disabled access needs Disabled access needs h) Don’t know Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more? Does this person's condition affect their dexterity? Does this person's condition affect their learning or understanding or concentrating? Does this person's condition affect their hearing? Does this person's condition affect their memory? Does this person's condition affect their mental health? Does this person's condition affect their mobility? Does this person's condition affect them socially or behaviourally? Does this person's condition affect their stamina or breathing or fatigue? Does this person's condition affect their vision? Does this person's condition affect them in another way? How long has the household continuously lived in the local authority area of the new letting? How long has the household been on the local authority housing register (or waiting list) for the area of the new letting? What is the tenant’s main reason for the household leaving their last settled home? If 'Other', what was the main reason for leaving their last settled home? Where was the household immediately before this letting? Did the household experience homelessness immediately before this letting? Do you know the postcode of the household's last settled home? Part 1 of postcode of last settled home Part 2 of postcode of last settled home What is the local authority of the household's last settled home? Was the household given 'reasonable preference' by the local authority? Reasonable preference reason They were homeless or about to lose their home (within 56 days) Reasonable preference reason They were living in unsanitary, overcrowded or unsatisfactory housing Reasonable preference reason They needed to move due to medical and welfare reasons (including disability) Reasonable preference reason They needed to move to avoid hardship to themselves or others Reasonable preference reason Don't know How was this letting allocated? Choice based Lettings (CBL) How was this letting allocated? Common Allocations Policy (CAP) How was this letting allocated? Common Housing Register (CHR) How was this letting allocated? Accessible Housing Register What was the source of referral for this letting? Do you know the household's combined total income after tax? How often does the household receive income? How much income does the household have in total? Is the tenant likely to be receiving any of these housing-related benefits? How much of the household's income is from Universal Credit, state pensions or benefits? Does the household pay rent or other charges for the accommodation? How often does the household pay rent and other charges? What is the basic rent? What is the service charge? What is the personal service charge? What is the support charge? After the household has received any housing-related benefits, will they still need to pay for rent and charges? What do you expect the outstanding amount to be?
3 Additional info You can find the org ID on the CORE service under 'Stock owners' or, if your organisation is the stock owner, under 'About your organisation' You can find the org ID on the CORE service under 'Managing agents' or, if your organisation is the managing agent, under 'About your organisation' If left empty, the letting log will be assigned to the account used to upload the log. General needs housing includes both self-contained and shared housing without support or specific adaptations. Supported housing includes direct access hostels, group homes, residential care and nursing homes. Scheme code. Include the 'S' at the beginning if it has one. You can find the scheme code on the CORE service under 'Schemes', either by searching for the specific scheme or downloading a csv. Location code. You can find the location code on the CORE service under 'Schemes', either by searching for the specific location or downloading a csv. If the property was previously being used as temporary accommodation, then answer 'no'. See specification for definitions This is how you usually refer to this tenancy on your own systems. This is how you usually refer to this property on your own systems. Make sure the lead tenant has seen or been given access to the Ministry of Housing, Communities and Local Government (MHCLG) privacy notice before completing this log. This is a legal requirement under data protection legislation. Internal transfer - Where a tenant moved from one social housing property to another property. Their landlord may be the same or may have changed. Renewal of a fixed term tenancy - to the same tenant in the same property, except if was previously used as temporary accommodation. This is the rent type of the previous tenancy in this property. The Unique Property Reference Number (UPRN) is a unique number system created by Ordnance Survey and used by housing providers and various industries across the UK. An example UPRN is 10010457355. The UPRN may not be the same as the property reference assigned by your organisation. Combined with field 22 it should be a postcode which lies within the local authority given in field 25. Combined with field 21 it should be a postcode which lies within the local authority given in field 25. This is whether someone who uses a wheelchair is able to make full use of all of the property’s rooms and facilities, including use of both inside and outside space, and entering and exiting the property. If shared accommodation, enter the number of bedrooms occupied by this household. A bedsit has 1 bedroom. Date the property was (legally/contractually) available to let, or for: - re-lets: the day after previous tenant’s contract end - new-builds: the day the landlord legally owned the property ('completion date’) - new conversions or acquisitions: the completion date, or the day after any rehabilitation work ended - new leases: the day the landlord got contractual property rights, and could let it out to tenants. Major repairs are works that could not be reasonably carried out with a tenant living at the property. For example, structural repairs. This includes retirement living, sheltered housing and extra care housing. There is no national set limit for “older people”, please answer based on your own policies. Extra care housing is for tenants with medium to high care and support needs, often with 24 hour access to support staff provided by an agency registered with the Care Quality Commission. This is where two or more people are named on the tenancy agreement. If the tenancy has an ‘introductory period’ answer ‘yes’. You should submit a CORE log at the beginning of the starter tenancy or introductory period, with the best information you have at the time. You do not need to submit a log when a tenant later rolls onto the main tenancy. This is about the main tenancy after any starter or introductory period. See specification for definitions. Do not include the starter or introductory period. The minimum period is 2 years for social or affordable rent general needs logs. You do not need to submit CORE logs for these types of tenancies if they are shorter than 2 years. This is the household member who does the most paid work. If several people do the same amount of paid work, it's the oldest household member. This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth. If the lead tenant is a dual national of the United Kingdom and another country, enter United Kingdom. If they are a dual national of two other countries, the tenant should decide which country to enter. This is the household member who does the most paid work. If several people do the same amount of paid work, it's the oldest household member. Answer 1 for children aged under 1 year old This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth. Answer 1 for children aged under 1 year old This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth. Answer 1 for children aged under 1 year old This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth. Answer 1 for children aged under 1 year old This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth. Answer 1 for children aged under 1 year old This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth. Answer 1 for children aged under 1 year old This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth. Answer 1 for children aged under 1 year old This should be however they personally choose to identify from the options below. This may or may not be the same as their biological sex or the sex they were assigned at birth. This excludes national service. If several household members have these links, answer for regular first. If no regular, answer for reserve. If no reserve, answer for spouses or civil partners. For example, lifting and carrying objects, or using a keyboard For example, deafness or partial hearing For example, depression or anxiety For example, walking short distances or climbing stairs For example, anything associated with autism spectrum disorder (ASD), including Asperger’s or attention deficit hyperactivity disorder (ADHD) For example, blindness or partial sight The tenant's ‘last settled home' is their last long-standing home. For tenants who had temporary accommodation, sleeping rough or otherwise homeless, their last settled home is where they were living previously. This is the tenant’s last long-standing home. It is where the tenant was living before any period in temporary accommodation, sleeping rough or otherwise homeless. Combined with field 104, it should be a postcode which lies within the local authority given in field 105. Combined with field 103, it should be a postcode which lies within the local authority given in field 105. This is the tenant’s last long-standing home. It is where the tenant was living before any period in temporary accommodation, sleeping rough or otherwise homeless. Households may be given ‘reasonable preference’ for social housing under one or more specific category by the local authority. This is also known as ‘priority need’. Where available vacant properties are advertised and applicants are able to bid for specific properties. Where a common system agreed between a group of housing providers is used to determine applicants' priority for housing. Where a single waiting list is used by a group of housing providers to receive and process housing applications. Providers may use different approaches to determine priority. Where the 'access category' or another descriptor of whether an available vacant property meets a range of access needs is displayed to applicants during the allocations process. Include any income after tax from employment, pensions, and Universal Credit. Don't include National Insurance (NI) contributions and tax, housing benefit, child benefit, or council tax support. This is about when the tenant is in their new let. If they are unsure about the situation for their new let and their financial and working situation hasn’t changed significantly, answer based on what housing-related benefits they currently receive. If rent is charged on the property then answer Yes, even if tenants do not pay it themselves. This is the amount paid before any charges are added for services (for example, hot water or cleaning). Households may receive housing benefit or Universal Credit towards basic rent. For example, cleaning. Households may get household benefits towards the service charge. For example heating or hot water. This doesn’t include housing benefit or Universal Credit. Any charges made to fund support services included in the tenancy agreement. Also known as the 'outstanding amount' You only need to give an approximate figure.
4 Values Alphanumeric Email format 01-Feb Alphanumeric Numeric 01-Feb Jan-31 01-Dec 25 - 26 01-Jul Text Alphanumeric, max 13 characters Alphanumeric, max 12 characters 1 5 - 6, or 8 - 22 1 - 3 or 5 - 9 Numeric Alphanumeric Text Alphanumeric, 2 - 4 characters Alphanumeric, 3 characters 9 character ONS code, beginning with 'E' (https://www.get-information-schools.service.gov.uk/Guidance/LaNameCodes) 1 - 2, 4 or 6 - 10 01-Feb 01-Feb 01-Jul Jan-31 01-Dec Jun-26 Jan-31 01-Dec Jun-26 2 - 4, 7 - 8 01-Mar 01-Feb 02-Aug Text 1 - 99, see specification for more detail 16 - 120 or R F, M, X or R Jan-20 3 digit ISO country code, see specification 0 - 10 01-Mar Numeric, range 1 - 120 or text (upper case 'R') Must be >= 16 if working situation = 1 - 8 or 0 Must be <16 if working situation = 9 F, M, X or R 0 - 10 Must be 9 if age <16 01-Mar Numeric, range 1 - 120 or text (upper case 'R') Must be >= 16 if working situation = 1 - 8 or 0 Must be <16 if working situation = 9 F, M, X or R 0 - 10 Must be 9 if age <16 01-Mar Numeric, range 1 - 120 or text (upper case 'R') Must be >= 16 if working situation = 1 - 8 or 0 Must be <16 if working situation = 9 F, M, X or R 0 - 10 Must be 9 if age <16 01-Mar Numeric, range 1 - 120 or text (upper case 'R') Must be >= 16 if working situation = 1 - 8 or 0 Must be <16 if working situation = 9 F, M, X or R 0 - 10 Must be 9 if age <16 01-Mar Numeric, range 1 - 120 or text (upper case 'R') Must be >= 16 if working situation = 1 - 8 or 0 Must be <16 if working situation = 9 F, M, X or R 0 - 10 Must be 9 if age <16 01-Mar Numeric, range 1 - 120 or text (upper case 'R') Must be >= 16 if working situation = 1 - 8 or 0 Must be <16 if working situation = 9 F, M, X or R 0 - 10 Must be 9 if age <16 01-Mar Numeric, range 1 - 120 or text (upper case 'R') Must be >= 16 if working situation = 1 - 8 or 0 Must be <16 if working situation = 9 F, M, X or R 0 - 10 Must be 9 if age <16 01-Jun 03-Jun 01-Mar 1 or empty 01-Mar 1 or empty 1 - 2 or 6 - 12 2 or 6 - 13 1 - 2, 4, 8 - 14, 16 - 20, 28 - 31, 34 or 44 - 55 Text 3 - 4, 6 - 7, 9 - 10, 13 - 14, 18 - 19, 21, 23 - 33, 35, 37 - 39 1 or 11 01-Feb Alphanumeric, 2 - 4 characters Alphanumeric, 3 characters 9 character ONS code, beginning with 'E' (https://www.get-information-schools.service.gov.uk/Guidance/LaNameCodes) 01-Mar 1 or empty 01-Feb 1 - 4, 7 - 10, 14 - 24 01-Mar 01-Mar 0 - 99999 1, 3, 6, 9 or 10 01-Apr 0 - 1 01-Oct xxxx.xx 01-Mar xxxx.xx
5 Can be empty? No Yes No Yes, if letting is general needs (if field 4 = 1) Yes, if letting is general needs (if field 4 = 1) No Yes, if letting is not 'Other intermediate rent product' (if field 11 is not 6) Yes No Yes, if letting is a renewal (if field 7 = 1) Yes, if letting is a renewal (if field 7 = 1) or a first-time let (if field 16 = 15 - 17) Yes, if letting is supported housing (if field 4 = 2) or if the property's postcode is not empty (if fields 23 and 24 contain full and valid entries) Yes, if letting is supported housing (if field 4 = 2) or if property's UPRN and local authority are known (if fields 18 and 25 are not empty) Yes Yes, if letting is supported housing (if field 4 = 2) or if property's UPRN and local authority are known (if fields 18 and 25 are not empty) Yes Yes, if letting is supported housing (if field 4 = 2) or if property's UPRN and local authority are known (if fields 18 and 25 are not empty) Yes, if letting is supported housing (if field 4 = 2) Yes, if letting is a renewal (if field 7 = 1) Yes Yes, if letting is general needs (if field 4 = 1) No Yes, if 'Other' is not selected for tenancy type (if field 39 is not 3) Yes, if letting is not a fixed-term tenancy (if field 39 is not 4 or 6) No Yes, if all fields about person 2 are empty (fields 47 - 50) Yes, if all fields about person 3 are empty (fields 51 - 54) Yes, if all fields about person 4 are empty (fields 55 - 58) Yes, if all fields about person 5 are empty (fields 59 - 62) Yes, if all fields about person 6 are empty (fields 63 - 66) Yes, if all fields about person 7 are empty (fields 67 - 70) Yes, if all fields about person 8 are empty (fields 71 - 74) No Yes, if no one in the household is a current or former regular (if field 75 is not 1) Yes, if no one in the household is a current or former regular or reserve (if field 75 is not 1 or 4) No Yes, if no household members have access needs or if it is unknown (if field 83 or 84 = 1) Yes, if a household member has an access need (if at least one of fields 79 to 82 = 1) No Yes, if a household member has an access need (if at least one of fields 79 to 82 = 1) If someone in the household does have such a condition (if field 89 = 1), then at least 1 of these fields must be 1. No Yes, if letting is a renewal (if field 7 = 1) No Yes, if 'Other' is not selected for reason for leaving last settled home (if field 98 is not 20) No No Yes, if postcode of household's last settled home is not known (if 102 = 2) Yes No If household was given 'reasonable preference' (if field 107 = 1), at least one of these fields must be 1 If household was not given 'reasonable preference' (if field 106 = 2 or 3), these fields will be ignored. No Yes, if letting is a renewal (if field 7 = 1) No Yes, if household's income is unknown (if field 117 = 2 or 3) No Yes, if letting is supported housing (if field 4 = 2) No Yes, if the household does not pay rent (if field 122 = 1) Yes, if the household doesn't receive housing benefits, or if it is unknown (if field 120 = 3, 9 or 10) Yes, if the household does not need to pay rent or charges after receiving housing benefits (if field 128 is not 1)
6 Type of letting the question applies to Supported housing only Other Intermediate Rent only General needs only Supported housing only Supported housing only
7 Duplicate check field? Yes Yes Yes Yes Yes Yes Yes
8 Field number 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
9 ORG1 ORG1 support@example.com 1 2 1 4 26 1 1 1 1 5 1 a a a a a1 1aa E09000001 1 1 1 1 1 4 25 3 1 2 20 F 1 GBR 1 1 6 3 1 1 1 1 1 2 50 30 1 2 1 1 1 1 1 1 7 2 1 2 1 1 50 0 0 0 3

23
spec/fixtures/files/2026_27_sales_bulk_upload.csv vendored

File diff suppressed because one or more lines are too long

7
spec/models/bulk_upload_spec.rb

@ -1,6 +1,8 @@
require "rails_helper"
RSpec.describe BulkUpload, type: :model do
include CollectionTimeHelper
let(:bulk_upload) { create(:bulk_upload, log_type: "lettings") }
describe "def bulk_upload.completed?" do
@ -21,7 +23,7 @@ RSpec.describe BulkUpload, type: :model do
describe "value check clearing" do
context "with a lettings log bulk upload" do
let(:log) { build(:lettings_log, startdate: Time.zone.local(2025, 4, 2), bulk_upload:) }
let(:log) { build(:lettings_log, startdate: current_collection_start_date, bulk_upload:) }
it "has the correct number of value checks to be set as confirmed" do
expect(bulk_upload.fields_to_confirm(log)).to match_array %w[rent_value_check void_date_value_check major_repairs_date_value_check pregnancy_value_check retirement_value_check referral_value_check net_income_value_check scharge_value_check pscharge_value_check supcharg_value_check multiple_partners_value_check partner_under_16_value_check reasonother_value_check]
@ -29,7 +31,7 @@ RSpec.describe BulkUpload, type: :model do
end
context "with a sales log bulk upload" do
let(:log) { build(:sales_log, saledate: Time.zone.local(2025, 4, 2), bulk_upload:) }
let(:log) { build(:sales_log, saledate: current_collection_start_date, bulk_upload:) }
it "has the correct number of value checks to be set as confirmed" do
expect(bulk_upload.fields_to_confirm(log)).to match_array %w[value_value_check monthly_charges_value_check percentage_discount_value_check income1_value_check income2_value_check combined_income_value_check retirement_value_check old_persons_shared_ownership_value_check buyer_livein_value_check wheel_value_check mortgage_value_check savings_value_check deposit_value_check staircase_bought_value_check stairowned_value_check hodate_check shared_ownership_deposit_value_check extrabor_value_check grant_value_check discounted_sale_value_check deposit_and_mortgage_value_check multiple_partners_value_check partner_under_16_value_check]
@ -42,6 +44,7 @@ RSpec.describe BulkUpload, type: :model do
{ year: 2023, expected_value: "2023 to 2024" },
{ year: 2024, expected_value: "2024 to 2025" },
{ year: 2025, expected_value: "2025 to 2026" },
{ year: 2026, expected_value: "2026 to 2027" },
].each do |test_case|
context "when the bulk upload year is #{test_case[:year]}" do
let(:bulk_upload) { build(:bulk_upload, year: test_case[:year]) }

12
spec/services/bulk_upload/lettings/log_creator_spec.rb

@ -1,18 +1,22 @@
require "rails_helper"
RSpec.describe BulkUpload::Lettings::LogCreator do
include CollectionTimeHelper
subject(:service) { described_class.new(bulk_upload:, path: "") }
let(:year) { current_collection_start_year }
let(:owning_org) { create(:organisation, old_visible_id: 123, rent_periods: [2]) }
let(:user) { create(:user, organisation: owning_org) }
let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year: 2024) }
let(:csv_parser) { instance_double(BulkUpload::Lettings::Year2024::CsvParser) }
let(:row_parser) { instance_double(BulkUpload::Lettings::Year2024::RowParser) }
let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year:) }
let(:csv_parser) { instance_double("BulkUpload::Lettings::Year#{year}::CsvParser") }
let(:row_parser) { instance_double("BulkUpload::Lettings::Year#{year}::RowParser") }
let(:log) { build(:lettings_log, :completed, assigned_to: user, owning_organisation: owning_org, managing_organisation: owning_org) }
before do
allow(BulkUpload::Lettings::Year2024::CsvParser).to receive(:new).and_return(csv_parser)
allow(Object.const_get("BulkUpload::Lettings::Year#{year}::CsvParser")).to receive(:new).and_return(csv_parser)
allow(csv_parser).to receive(:row_parsers).and_return([row_parser])
allow(row_parser).to receive(:log).and_return(log)
allow(row_parser).to receive(:bulk_upload=).and_return(true)

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

@ -1,8 +1,12 @@
require "rails_helper"
RSpec.describe BulkUpload::Lettings::Validator do
include CollectionTimeHelper
subject(:validator) { described_class.new(bulk_upload:, path:) }
let(:year) { current_collection_start_year }
let(:date) { current_collection_start_date }
let(:organisation) { create(:organisation, old_visible_id: "3", rent_periods: [2]) }
let(:user) { create(:user, organisation:) }
let(:log) { build(:lettings_log, :completed, period: 2, assigned_to: user) }
@ -73,14 +77,14 @@ RSpec.describe BulkUpload::Lettings::Validator do
end
end
context "when uploading a 2023 logs for 2024 bulk upload" do
let(:log) { build(:lettings_log, :completed, startdate: Time.zone.local(2023, 5, 6), tenancycode: "5234234234234") }
let(:bulk_upload) { build(:bulk_upload, user:, year: 2024) }
context "when uploading a previous logs for current bulk upload" do
let(:log) { build(:lettings_log, :completed, startdate: previous_collection_start_date, tenancycode: "5234234234234") }
let(:bulk_upload) { build(:bulk_upload, user:, year:) }
context "with headers" do
let(:seed) { rand }
let(:field_numbers) { log_to_csv.default_2024_field_numbers }
let(:field_values) { log_to_csv.to_2024_row }
let(:field_numbers) { log_to_csv.send("default_#{year}_field_numbers") }
let(:field_values) { log_to_csv.send("to_#{year}_row") }
before do
file.write(log_to_csv.custom_field_numbers_row(seed:, field_numbers:))
@ -97,13 +101,13 @@ RSpec.describe BulkUpload::Lettings::Validator do
end
describe "#call" do
context "with an invalid 2024 csv" do
let(:log) { build(:lettings_log, :completed, startdate: Time.zone.local(2024, 7, 1), period: 2, assigned_to: user) }
context "with an invalid csv" do
let(:log) { build(:lettings_log, :completed, startdate: date, period: 2, assigned_to: user) }
before do
values = log_to_csv.to_2024_row
values = log_to_csv.send("to_#{year}_row")
values[7] = nil
file.write(log_to_csv.default_field_numbers_row_for_year(2024))
file.write(log_to_csv.default_field_numbers_row_for_year(year))
file.write(log_to_csv.to_custom_csv_row(seed: nil, field_values: values))
file.rewind
end
@ -141,12 +145,12 @@ RSpec.describe BulkUpload::Lettings::Validator do
context "with arbitrary ordered invalid csv" do
let(:seed) { 321 }
let(:log) { build(:lettings_log, :completed, startdate: Time.zone.local(2024, 7, 1), period: 2, assigned_to: user) }
let(:log) { build(:lettings_log, :completed, startdate: date, period: 2, assigned_to: user) }
before do
log.needstype = nil
values = log_to_csv.to_2024_row
file.write(log_to_csv.default_field_numbers_row_for_year(2024, seed:))
values = log_to_csv.send("to_#{year}_row")
file.write(log_to_csv.default_field_numbers_row_for_year(year, seed:))
file.write(log_to_csv.to_custom_csv_row(seed:, field_values: values))
file.close
end
@ -165,8 +169,8 @@ RSpec.describe BulkUpload::Lettings::Validator do
expect(error.tenant_code).to eql(log.tenancycode)
expect(error.property_ref).to eql(log.propcode)
expect(error.row).to eql("2")
expect(error.cell).to eql("CY2")
expect(error.col).to eql("CY")
expect(error.cell).to eql("CX2")
expect(error.col).to eql("CX")
end
end
@ -218,8 +222,8 @@ RSpec.describe BulkUpload::Lettings::Validator do
end
end
context "with a 2024 csv without headers" do
let(:log) { build(:lettings_log, :completed, startdate: Time.zone.local(2024, 7, 1), period: 2, assigned_to: user) }
context "with a csv without headers" do
let(:log) { build(:lettings_log, :completed, startdate: date, period: 2, assigned_to: user) }
before do
file.write(log_to_csv.to_csv_row)

254
spec/services/bulk_upload/lettings/year2026/csv_parser_spec.rb

@ -0,0 +1,254 @@
require "rails_helper"
RSpec.describe BulkUpload::Lettings::Year2026::CsvParser do
subject(:service) { described_class.new(path:) }
let(:file) { Tempfile.new }
let(:path) { file.path }
let(:log) { build(:lettings_log, :completed) }
context "when parsing csv with headers" do
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2026))
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(7)
expect(service.col_offset).to eq(1)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
end
context "when some csv headers are empty (and we don't care about them)" do
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2026))
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(7)
expect(service.col_offset).to eq(1)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
end
context "when parsing csv with headers with extra rows" do
before do
file.write("Section\n")
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2026))
file.write("\n")
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(8)
expect(service.col_offset).to eq(1)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
it "does not parse the last empty row" do
expect(service.row_parsers.count).to eq(1)
end
end
context "when parsing csv with headers in arbitrary order" do
let(:seed) { rand }
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2026, seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2026, seed:))
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(7)
expect(service.col_offset).to eq(1)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
end
context "when parsing csv with extra invalid headers" do
let(:seed) { rand }
let(:log_to_csv) { BulkUpload::LettingsLogToCsv.new(log:) }
let(:field_numbers) { log_to_csv.default_2026_field_numbers + %w[invalid_field_number] }
let(:field_values) { log_to_csv.to_2026_row + %w[value_for_invalid_field_number] }
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(log_to_csv.custom_field_numbers_row(seed:, field_numbers:))
file.write(log_to_csv.to_custom_csv_row(seed:, field_values:))
file.rewind
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
it "counts the number of valid field numbers correctly" do
expect(service).to be_correct_field_count
end
end
context "when parsing csv without headers" do
before do
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2026))
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(0)
expect(service.col_offset).to eq(0)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
end
context "when parsing with BOM aka byte order mark" do
let(:bom) { "\uFEFF" }
before do
file.write(bom)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2026))
file.rewind
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
end
context "when an invalid byte sequence" do
let(:invalid_sequence) { "\x81" }
before do
file.write(invalid_sequence)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2026))
file.rewind
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
end
context "when parsing csv with carriage returns" do
before do
file.write("Question\r\n")
file.write("Additional info\r")
file.write("Values\r\n")
file.write("Can be empty?\r")
file.write("Type of letting the question applies to\r\n")
file.write("Duplicate check field?\r")
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2026))
file.rewind
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_13).to eql(log.tenancycode)
end
end
describe "#column_for_field", aggregate_failures: true do
context "when with headers using default ordering" do
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2026))
file.rewind
end
it "returns correct column" do
expect(service.column_for_field("field_5")).to eql("F")
expect(service.column_for_field("field_22")).to eql("W")
end
end
context "when without headers using default ordering" do
before do
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2026))
file.rewind
end
it "returns correct column" do
expect(service.column_for_field("field_5")).to eql("E")
expect(service.column_for_field("field_22")).to eql("V")
end
end
context "when with headers using custom ordering" do
let(:seed) { 123 }
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2026, seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2026, seed:))
file.rewind
end
it "returns correct column" do
expect(service.column_for_field("field_5")).to eql("B")
expect(service.column_for_field("field_22")).to eql("AS")
expect(service.column_for_field("field_26")).to eql("DG")
expect(service.column_for_field("field_25")).to eql("I")
end
end
end
end

2882
spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb

File diff suppressed because it is too large Load Diff

316
spec/services/bulk_upload/sales/log_creator_spec.rb

@ -1,187 +1,171 @@
require "rails_helper"
RSpec.describe BulkUpload::Sales::LogCreator do
include CollectionTimeHelper
subject(:service) { described_class.new(bulk_upload:, path: "") }
let(:year) { current_collection_start_year }
let(:owning_org) { create(:organisation, old_visible_id: 123) }
let(:user) { create(:user, organisation: owning_org) }
let(:log) { build(:sales_log, :completed, assigned_to: user, owning_organisation: owning_org, managing_organisation: owning_org) }
[2023, 2024, 2025].each do |year|
context "when #{year}" do
let(:bulk_upload) { create(:bulk_upload, :sales, user:, year:) }
let(:year_csv_parser) { instance_double("BulkUpload::Sales::Year#{year}::CsvParser".constantize) }
let(:year_row_parser) { instance_double("BulkUpload::Sales::Year#{year}::RowParser".constantize) }
let(:bulk_upload) { create(:bulk_upload, :sales, user:, year:) }
let(:csv_parser) { instance_double("BulkUpload::Sales::Year#{year}::CsvParser".constantize) }
let(:row_parser) { instance_double("BulkUpload::Sales::Year#{year}::RowParser".constantize) }
before do
allow("BulkUpload::Sales::Year#{year}::CsvParser".constantize).to receive(:new).and_return(csv_parser)
allow(csv_parser).to receive(:row_parsers).and_return([row_parser])
allow(row_parser).to receive(:log).and_return(log)
allow(row_parser).to receive(:bulk_upload=).and_return(true)
allow(row_parser).to receive(:valid?).and_return(true)
allow(row_parser).to receive(:blank_row?).and_return(false)
end
it "creates a parser for the correct year" do
# This would fail without parser stubs, so the parser must be for the expected year
expect { service.call }.to change(SalesLog, :count)
end
describe "#call" do
context "when a valid csv with new log" do
it "creates a new log" do
expect { service.call }.to change(SalesLog, :count)
end
it "create a log with pending status" do
service.call
expect(SalesLog.last.status).to eql("pending")
end
it "associates log with bulk upload" do
service.call
log = SalesLog.last
expect(log.bulk_upload).to eql(bulk_upload)
expect(bulk_upload.sales_logs).to include(log)
end
it "sets the creation method" do
service.call
expect(SalesLog.last.creation_method_bulk_upload?).to be true
end
end
context "when a valid csv with several blank rows" do
before do
allow("BulkUpload::Sales::Year#{year}::CsvParser".constantize).to receive(:new).and_return(year_csv_parser)
allow(year_csv_parser).to receive(:row_parsers).and_return([year_row_parser])
allow(year_row_parser).to receive(:log).and_return(log)
allow(year_row_parser).to receive(:bulk_upload=).and_return(true)
allow(year_row_parser).to receive(:valid?).and_return(true)
allow(year_row_parser).to receive(:blank_row?).and_return(false)
allow(row_parser).to receive(:blank_row?).and_return(true)
end
it "creates a parser for the correct year" do
# This would fail without parser stubs, so the parser must be for the expected year
expect { service.call }.to change(SalesLog, :count)
it "ignores them and does not create the logs" do
expect { service.call }.not_to change(SalesLog, :count)
end
end
end
# Apart from picking the correct year's parser, everything else is year-independent
context "when 2024" do
let(:bulk_upload) { create(:bulk_upload, :sales, user:, year: 2024) }
let(:csv_parser) { instance_double(BulkUpload::Sales::Year2024::CsvParser) }
let(:row_parser) { instance_double(BulkUpload::Sales::Year2024::RowParser) }
before do
allow(BulkUpload::Sales::Year2024::CsvParser).to receive(:new).and_return(csv_parser)
allow(csv_parser).to receive(:row_parsers).and_return([row_parser])
allow(row_parser).to receive(:log).and_return(log)
allow(row_parser).to receive(:bulk_upload=).and_return(true)
allow(row_parser).to receive(:valid?).and_return(true)
allow(row_parser).to receive(:blank_row?).and_return(false)
context "when a valid csv with row with one invalid non setup field" do
let(:log) do
build(
:sales_log,
:completed,
age1: 5,
owning_organisation: owning_org,
assigned_to: user,
managing_organisation: owning_org,
)
end
it "creates the log" do
expect { service.call }.to change(SalesLog, :count).by(1)
end
it "blanks invalid field" do
service.call
record = SalesLog.last
expect(record.age1).to be_blank
end
end
describe "#call" do
context "when a valid csv with new log" do
it "creates a new log" do
expect { service.call }.to change(SalesLog, :count)
end
it "create a log with pending status" do
service.call
expect(SalesLog.last.status).to eql("pending")
end
it "associates log with bulk upload" do
service.call
log = SalesLog.last
expect(log.bulk_upload).to eql(bulk_upload)
expect(bulk_upload.sales_logs).to include(log)
end
it "sets the creation method" do
service.call
expect(SalesLog.last.creation_method_bulk_upload?).to be true
end
end
context "when a valid csv with several blank rows" do
before do
allow(row_parser).to receive(:blank_row?).and_return(true)
end
it "ignores them and does not create the logs" do
expect { service.call }.not_to change(SalesLog, :count)
end
end
context "when a valid csv with row with one invalid non setup field" do
let(:log) do
build(
:sales_log,
:completed,
age1: 5,
owning_organisation: owning_org,
assigned_to: user,
managing_organisation: owning_org,
)
end
it "creates the log" do
expect { service.call }.to change(SalesLog, :count).by(1)
end
it "blanks invalid field" do
service.call
record = SalesLog.last
expect(record.age1).to be_blank
end
end
context "when a valid csv with row with compound errors on non setup field" do
let(:log) do
build(
:sales_log,
:completed,
owning_organisation: owning_org,
assigned_to: user,
managing_organisation: owning_org,
ownershipsch: 2,
value: 200_000,
deposit: 10_000,
mortgageused: 1,
mortgage: 100_000,
grant: 10_000,
)
end
it "creates the log" do
expect { service.call }.to change(SalesLog, :count).by(1)
end
it "blanks invalid field" do
service.call
record = SalesLog.last
expect(record.value).to be_blank
expect(record.deposit).to be_blank
expect(record.mortgage).to be_blank
expect(record.grant).to be_blank
end
end
context "when pre-creating logs" do
it "creates a new log" do
expect { service.call }.to change(SalesLog, :count)
end
it "creates a log with correct states" do
service.call
last_log = SalesLog.last
expect(last_log.status).to eql("pending")
expect(last_log.status_cache).to eql("completed")
end
end
context "with a valid csv and soft validations" do
let(:log) do
build(
:sales_log,
:completed,
age1: 30,
age1_known: 0,
ecstat1: 5,
owning_organisation: owning_org,
assigned_to: user,
managing_organisation: owning_org,
)
end
it "creates a new log" do
expect { service.call }.to change(SalesLog, :count)
end
it "creates a log with pending status" do
service.call
expect(SalesLog.last.status).to eql("pending")
end
it "does not set unanswered soft validations" do
service.call
log = SalesLog.last
expect(log.age1).to be(30)
expect(log.ecstat1).to be(5)
expect(log.retirement_value_check).to be(nil)
end
context "when a valid csv with row with compound errors on non setup field" do
let(:log) do
build(
:sales_log,
:completed,
owning_organisation: owning_org,
assigned_to: user,
managing_organisation: owning_org,
ownershipsch: 2,
value: 200_000,
deposit: 10_000,
mortgageused: 1,
mortgage: 100_000,
grant: 10_000,
)
end
it "creates the log" do
expect { service.call }.to change(SalesLog, :count).by(1)
end
it "blanks invalid field" do
service.call
record = SalesLog.last
expect(record.value).to be_blank
expect(record.deposit).to be_blank
expect(record.mortgage).to be_blank
expect(record.grant).to be_blank
end
end
context "when pre-creating logs" do
it "creates a new log" do
expect { service.call }.to change(SalesLog, :count)
end
it "creates a log with correct states" do
service.call
last_log = SalesLog.last
expect(last_log.status).to eql("pending")
expect(last_log.status_cache).to eql("completed")
end
end
context "with a valid csv and soft validations" do
let(:log) do
build(
:sales_log,
:completed,
age1: 30,
age1_known: 0,
ecstat1: 5,
owning_organisation: owning_org,
assigned_to: user,
managing_organisation: owning_org,
)
end
it "creates a new log" do
expect { service.call }.to change(SalesLog, :count)
end
it "creates a log with pending status" do
service.call
expect(SalesLog.last.status).to eql("pending")
end
it "does not set unanswered soft validations" do
service.call
log = SalesLog.last
expect(log.age1).to be(30)
expect(log.ecstat1).to be(5)
expect(log.retirement_value_check).to be(nil)
end
end
end

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

@ -1,8 +1,12 @@
require "rails_helper"
RSpec.describe BulkUpload::Sales::Validator do
include CollectionTimeHelper
subject(:validator) { described_class.new(bulk_upload:, path:) }
let(:year) { current_collection_start_year }
let(:date) { current_collection_start_date }
let(:user) { create(:user, organisation:) }
let(:organisation) { create(:organisation, old_visible_id: "123") }
let(:log) { build(:sales_log, :completed, assigned_to: user) }
@ -15,7 +19,7 @@ RSpec.describe BulkUpload::Sales::Validator do
context "when file is empty" do
it "is not valid" do
expect(validator).not_to be_valid
expect(validator.errors["base"]).to eql([I18n.t("validations.sales.2024.bulk_upload.blank_file")])
expect(validator.errors["base"]).to eql([I18n.t("validations.sales.#{year}.bulk_upload.blank_file")])
end
end
@ -27,50 +31,34 @@ RSpec.describe BulkUpload::Sales::Validator do
it "is not valid" do
expect(validator).not_to be_valid
expect(validator.errors["base"]).to eql([I18n.t("validations.sales.2024.bulk_upload.blank_file")])
expect(validator.errors["base"]).to eql([I18n.t("validations.sales.#{year}.bulk_upload.blank_file")])
end
end
context "when file has too many columns" do
before do
file.write((%w[a] * (BulkUpload::Sales::Year2023::CsvParser::MAX_COLUMNS + 1)).join(","))
file.rewind
end
it "is not valid" do
expect(validator).not_to be_valid
end
end
context "when trying to upload 2023 logs for 2024 bulk upload" do
let(:bulk_upload) { create(:bulk_upload, user:, year: 2024) }
let(:log) { build(:sales_log, :completed, saledate: Time.zone.local(2023, 10, 10), assigned_to: user) }
before do
file.write(log_to_csv.default_field_numbers_row_for_year(2024))
file.write(log_to_csv.to_year_csv_row(2024))
file.write((%w[a] * (Object.const_get("BulkUpload::Sales::Year#{year}::CsvParser::MAX_COLUMNS") + 1)).join(","))
file.rewind
end
it "is not valid" do
expect(validator).not_to be_valid
expect(validator.errors["base"]).to eql([I18n.t("validations.sales.2024.bulk_upload.wrong_template.wrong_template")])
end
end
context "when trying to upload 2024 logs for 2025 bulk upload" do
let(:bulk_upload) { create(:bulk_upload, user:, year: 2025) }
let(:log) { build(:sales_log, :completed, saledate: Time.zone.local(2024, 10, 10), assigned_to: user) }
context "when trying to upload previous logs for current bulk upload" do
let(:bulk_upload) { create(:bulk_upload, user:, year:) }
let(:log) { build(:sales_log, :completed, saledate: previous_collection_start_date, assigned_to: user) }
before do
file.write(log_to_csv.default_field_numbers_row_for_year(2025))
file.write(log_to_csv.to_year_csv_row(2025))
file.write(log_to_csv.default_field_numbers_row_for_year(year))
file.write(log_to_csv.to_year_csv_row(year))
file.rewind
end
it "is not valid" do
expect(validator).not_to be_valid
expect(validator.errors["base"]).to eql([I18n.t("validations.sales.2025.bulk_upload.wrong_template.wrong_template")])
expect(validator.errors["base"]).to eql([I18n.t("validations.sales.#{year}.bulk_upload.wrong_template.wrong_template")])
end
end
@ -135,10 +123,10 @@ RSpec.describe BulkUpload::Sales::Validator do
end
end
context "with an invalid 2024 csv" do
context "with an invalid csv" do
before do
log.owning_organisation = nil
log.saledate = Time.zone.local(2024, 10, 10)
log.saledate = date
file.write(log_to_csv.default_field_numbers_row)
file.write(log_to_csv.to_csv_row)
file.rewind
@ -151,14 +139,14 @@ RSpec.describe BulkUpload::Sales::Validator do
it "create validation error with correct values" do
validator.call
error = BulkUploadError.find_by(row: "2", field: "field_1", category: "setup")
error = BulkUploadError.find_by(row: "2", field: "field_4", category: "setup")
expect(error.field).to eql("field_1")
expect(error.field).to eql("field_4")
expect(error.error).to eql("You must answer owning organisation.")
expect(error.purchaser_code).to eql(log.purchaser_code)
expect(error.row).to eql("2")
expect(error.cell).to eql("B2")
expect(error.col).to eql("B")
expect(error.cell).to eql("E2")
expect(error.col).to eql("E")
end
end

191
spec/services/bulk_upload/sales/year2026/csv_parser_spec.rb

@ -0,0 +1,191 @@
require "rails_helper"
RSpec.describe BulkUpload::Sales::Year2026::CsvParser do
subject(:service) { described_class.new(path:) }
let(:file) { Tempfile.new }
let(:path) { file.path }
let(:log) { build(:sales_log, :completed, :with_uprn) }
context "when parsing csv with headers" do
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025))
file.write("\n")
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(7)
expect(service.col_offset).to eq(1)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_16).to eql(log.uprn)
end
it "counts the number of valid field numbers correctly" do
expect(service).to be_correct_field_count
end
it "does not parse the last empty row" do
expect(service.row_parsers.count).to eq(1)
end
end
context "when some csv headers are empty (and we don't care about them)" do
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2026))
file.write("\n")
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(7)
expect(service.col_offset).to eq(1)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_16).to eql(log.uprn)
end
it "counts the number of valid field numbers correctly" do
expect(service).to be_correct_field_count
end
it "does not parse the last empty row" do
expect(service.row_parsers.count).to eq(1)
end
end
context "when parsing csv with headers in arbitrary order" do
let(:seed) { rand }
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025, seed:))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025, seed:))
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(7)
expect(service.col_offset).to eq(1)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_16).to eql(log.uprn)
end
end
context "when parsing csv without headers" do
let(:file) { Tempfile.new }
let(:path) { file.path }
let(:log) { build(:sales_log, :completed, :with_uprn) }
before do
file.write(BulkUpload::SalesLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2026))
file.rewind
end
it "returns correct offsets" do
expect(service.row_offset).to eq(0)
expect(service.col_offset).to eq(0)
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_16).to eql(log.uprn)
end
end
context "when parsing with BOM aka byte order mark" do
let(:file) { Tempfile.new }
let(:path) { file.path }
let(:log) { build(:sales_log, :completed, :with_uprn) }
let(:bom) { "\uFEFF" }
before do
file.write(bom)
file.write(BulkUpload::SalesLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2026))
file.close
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_16).to eql(log.uprn)
end
end
context "when an invalid byte sequence" do
let(:file) { Tempfile.new }
let(:path) { file.path }
let(:log) { build(:sales_log, :completed, :with_uprn) }
let(:invalid_sequence) { "\x81" }
before do
file.write(invalid_sequence)
file.write(BulkUpload::SalesLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2026))
file.close
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_16).to eql(log.uprn)
end
end
describe "#column_for_field", aggregate_failures: true do
context "when headers present" do
before do
file.write("Question\n")
file.write("Additional info\n")
file.write("Values\n")
file.write("Can be empty?\n")
file.write("Type of letting the question applies to\n")
file.write("Duplicate check field?\n")
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2026))
file.rewind
end
it "returns correct column" do
expect(service.column_for_field("field_1")).to eql("B")
expect(service.column_for_field("field_99")).to eql("CV")
end
end
end
context "when parsing csv with carriage returns" do
before do
file.write("Question\r\n")
file.write("Additional info\r")
file.write("Values\r\n")
file.write("Can be empty?\r")
file.write("Type of letting the question applies to\r\n")
file.write("Duplicate check field?\r")
file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2026))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2026))
file.rewind
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_16).to eql(log.uprn)
end
end
end

1981
spec/services/bulk_upload/sales/year2026/row_parser_spec.rb

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save