Browse Source

Merge branch 'main' into CLDC-3787-Autocomplete-address-search

CLDC-3787-Autocomplete-address-search
Manny Dinssa 4 days ago
parent
commit
ea64eee28e
  1. 0
      .rake_tasks~
  2. 11
      app/components/create_log_actions_component.html.erb
  3. 4
      app/components/create_log_actions_component.rb
  4. 68
      app/controllers/test_data_controller.rb
  5. 4
      app/frontend/styles/_task-list.scss
  6. 259
      app/helpers/bulk_upload/lettings_log_to_csv.rb
  7. 139
      app/helpers/bulk_upload/sales_log_to_csv.rb
  8. 5
      app/helpers/collection_deadline_helper.rb
  9. 17
      app/helpers/tasklist_helper.rb
  10. 2
      app/models/bulk_upload.rb
  11. 9
      app/models/derived_variables/lettings_log_variables.rb
  12. 10
      app/models/derived_variables/sales_log_variables.rb
  13. 2
      app/models/form/sales/pages/living_before_purchase.rb
  14. 2
      app/models/form/sales/questions/living_before_purchase_years.rb
  15. 9
      app/models/forms/bulk_upload_form/prepare_your_file.rb
  16. 2
      app/services/bulk_upload/lettings/log_creator.rb
  17. 2
      app/services/bulk_upload/lettings/validator.rb
  18. 122
      app/services/bulk_upload/lettings/year2025/csv_parser.rb
  19. 1654
      app/services/bulk_upload/lettings/year2025/row_parser.rb
  20. 2
      app/services/bulk_upload/sales/log_creator.rb
  21. 2
      app/services/bulk_upload/sales/validator.rb
  22. 1
      app/services/bulk_upload/sales/year2024/row_parser.rb
  23. 124
      app/services/bulk_upload/sales/year2025/csv_parser.rb
  24. 1502
      app/services/bulk_upload/sales/year2025/row_parser.rb
  25. 4
      app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb
  26. 6
      app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb
  27. 2
      app/views/logs/edit.html.erb
  28. 2
      config/locales/en.yml
  29. 2
      config/locales/forms/2025/sales/sale_information.en.yml
  30. 60
      config/locales/validations/lettings/2025/bulk_upload.en.yml
  31. 46
      config/locales/validations/sales/2025/bulk_upload.en.yml
  32. 2
      config/routes.rb
  33. BIN
      session-manager-plugin.deb
  34. 45
      spec/helpers/tasklist_helper_spec.rb
  35. 1
      spec/models/bulk_upload_spec.rb
  36. 2
      spec/models/form/sales/pages/living_before_purchase_spec.rb
  37. 20
      spec/models/lettings_log_derived_fields_spec.rb
  38. 20
      spec/models/sales_log_derived_fields_spec.rb
  39. 15
      spec/requests/lettings_logs_controller_spec.rb
  40. 11
      spec/requests/sales_logs_controller_spec.rb
  41. 4
      spec/services/bulk_upload/lettings/validator_spec.rb
  42. 32
      spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb
  43. 36
      spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb
  44. 254
      spec/services/bulk_upload/lettings/year2025/csv_parser_spec.rb
  45. 2808
      spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb
  46. 309
      spec/services/bulk_upload/sales/log_creator_spec.rb
  47. 16
      spec/services/bulk_upload/sales/validator_spec.rb
  48. 191
      spec/services/bulk_upload/sales/year2025/csv_parser_spec.rb
  49. 1915
      spec/services/bulk_upload/sales/year2025/row_parser_spec.rb

0
.rake_tasks~

11
app/components/create_log_actions_component.html.erb

@ -1,5 +1,5 @@
<div class="govuk-button-group app-filter-toggle <%= "govuk-!-margin-bottom-6" if display_actions? %>">
<% if display_actions? %>
<% if display_actions? %>
<%= govuk_button_to create_button_copy, create_button_href, class: "govuk-!-margin-right-3" %>
<% unless user.support? %>
<%= govuk_button_link_to upload_button_copy, upload_button_href, secondary: true %>
@ -9,9 +9,10 @@
<% end %>
<% if FeatureToggle.create_test_logs_enabled? %>
<%= govuk_link_to "Create test log", create_test_log_href %>
<%= govuk_link_to "Create test log (setup only)", create_setup_test_log_href %>
<%= govuk_link_to "Get test BU file (2024)", create_2024_test_bulk_upload_href %>
<%= govuk_link_to "New test log", create_test_log_href %>
<%= govuk_link_to "New test log (setup only)", create_setup_test_log_href %>
<%= govuk_link_to "24 BU test file", create_test_bulk_upload_href(2024) %>
<%= govuk_link_to "25 BU test file", create_test_bulk_upload_href(2025) %>
<% end %>
<% end %>
<% end %>
</div>

4
app/components/create_log_actions_component.rb

@ -42,8 +42,8 @@ class CreateLogActionsComponent < ViewComponent::Base
send("create_setup_test_#{log_type}_log_path")
end
def create_2024_test_bulk_upload_href
send("create_2024_test_#{log_type}_bulk_upload_path")
def create_test_bulk_upload_href(year)
send("create_#{year}_test_#{log_type}_bulk_upload_path")
end
def view_uploads_button_copy

68
app/controllers/test_data_controller.rb

@ -15,24 +15,28 @@ class TestDataController < ApplicationController
redirect_to lettings_log_path(log)
end
def create_2024_test_lettings_bulk_upload
return render_not_found unless FeatureToggle.create_test_logs_enabled?
%w[2024 2025].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("test_lettings_log.csv")
log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA")
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)
file.rewind
send_file file.path, type: "text/csv",
filename: "test_lettings_log.csv",
disposition: "attachment",
after_send: lambda {
file.close
file.unlink
}
file = Tempfile.new("#{year}_test_lettings_log.csv")
log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA", startdate: Time.zone.local(year.to_i, rand(4..12), rand(1..28)))
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)
file.rewind
send_file file.path, type: "text/csv",
filename: "#{year}_test_lettings_log.csv",
disposition: "attachment",
after_send: lambda {
file.close
file.unlink
}
end
end
def create_2025_test_sales_bulk_upload; end
def create_test_sales_log
return render_not_found unless FeatureToggle.create_test_logs_enabled?
@ -47,22 +51,24 @@ class TestDataController < ApplicationController
redirect_to sales_log_path(log)
end
def create_2024_test_sales_bulk_upload
return render_not_found unless FeatureToggle.create_test_logs_enabled?
file = Tempfile.new("test_sales_log.csv")
[2024, 2025].each do |year|
define_method("create_#{year}_test_sales_bulk_upload") do
return render_not_found unless FeatureToggle.create_test_logs_enabled?
log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, value: 180_000, deposit: 150_000)
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)
file.rewind
send_file file.path, type: "text/csv",
filename: "test_sales_log.csv",
disposition: "attachment",
after_send: lambda {
file.close
file.unlink
}
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: Time.zone.local(year.to_i, rand(4..12), rand(1..28)))
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)
file.rewind
send_file file.path,
type: "text/csv",
filename: "#{year}_test_sales_log.csv",
disposition: "attachment",
after_send: lambda {
file.close
file.unlink
}
end
end
end

4
app/frontend/styles/_task-list.scss

@ -52,3 +52,7 @@
margin-bottom: 0;
}
}
.app-red-text {
color: govuk-colour("red");
}

259
app/helpers/bulk_upload/lettings_log_to_csv.rb

@ -17,12 +17,8 @@ class BulkUpload::LettingsLogToCsv
def to_csv_row(seed: nil)
year = log.collection_start_year
case year
when 2022
to_2022_csv_row(seed:)
when 2023
to_2023_csv_row(seed:)
when 2024
to_2024_csv_row(seed:)
when 2022, 2023, 2024, 2025
to_year_csv_row(year, seed:)
else
raise NotImplementedError "No mapping function implemented for year #{year}"
end
@ -30,82 +26,32 @@ class BulkUpload::LettingsLogToCsv
def to_row
year = log.collection_start_year
case year
when 2022
to_2022_row
when 2023
to_2023_row
when 2024
to_2024_row
else
raise NotImplementedError "No mapping function implemented for year #{year}"
end
send("to_#{year}_row")
rescue NoMethodError
raise NotImplementedError "No mapping function implemented for year #{year}"
end
def default_field_numbers_row(seed: nil)
year = log.collection_start_year
case year
when 2022
default_2022_field_numbers_row(seed:)
when 2023
default_2023_field_numbers_row(seed:)
when 2024
default_2024_field_numbers_row(seed:)
else
raise NotImplementedError "No mapping function implemented for year #{year}"
end
default_field_numbers_row_for_year(year, seed:)
rescue NoMethodError
raise NotImplementedError "No mapping function implemented for year #{year}"
end
def default_field_numbers
year = log.collection_start_year
case year
when 2022
default_2022_field_numbers
when 2023
default_2023_field_numbers
when 2024
default_2024_field_numbers
else
raise NotImplementedError "No mapping function implemented for year #{year}"
end
send("default_#{year}_field_numbers")
rescue NoMethodError
raise NotImplementedError "No mapping function implemented for year #{year}"
end
def to_2022_csv_row(seed: nil)
def to_year_csv_row(year, seed: nil)
unshuffled_row = send("to_#{year}_row")
if seed
row = to_2022_row.shuffle(random: Random.new(seed))
row = unshuffled_row.shuffle(random: Random.new(seed))
(row_prefix + row).flatten.join(",") + line_ending
else
(row_prefix + to_2022_row).flatten.join(",") + line_ending
end
end
def default_2022_field_numbers
(1..134).to_a
end
def default_2022_field_numbers_row(seed: nil)
if seed
["Field number"] + default_2022_field_numbers.shuffle(random: Random.new(seed))
else
["Field number"] + default_2022_field_numbers
end.flatten.join(",") + line_ending
end
def to_2023_csv_row(seed: nil)
if seed
row = to_2023_row.shuffle(random: Random.new(seed))
(row_prefix + row).flatten.join(",") + line_ending
else
(row_prefix + to_2023_row).flatten.join(",") + line_ending
end
end
def to_2024_csv_row(seed: nil)
if seed
row = to_2024_row.shuffle(random: Random.new(seed))
(row_prefix + row).flatten.join(",") + line_ending
else
(row_prefix + to_2024_row).flatten.join(",") + line_ending
(row_prefix + unshuffled_row).flatten.join(",") + line_ending
end
end
@ -121,20 +67,16 @@ class BulkUpload::LettingsLogToCsv
]
end
def default_2023_field_numbers_row(seed: nil)
def default_field_numbers_row_for_year(year, seed: nil)
if seed
["Field number"] + default_2023_field_numbers.shuffle(random: Random.new(seed))
["Field number"] + send("default_#{year}_field_numbers").shuffle(random: Random.new(seed))
else
["Field number"] + default_2023_field_numbers
["Field number"] + send("default_#{year}_field_numbers")
end.flatten.join(",") + line_ending
end
def default_2024_field_numbers_row(seed: nil)
if seed
["Field number"] + default_2024_field_numbers.shuffle(random: Random.new(seed))
else
["Field number"] + default_2024_field_numbers
end.flatten.join(",") + line_ending
def default_2022_field_numbers
(1..134).to_a
end
def default_2023_field_numbers
@ -145,6 +87,156 @@ class BulkUpload::LettingsLogToCsv
(1..130).to_a
end
def default_2025_field_numbers
(1..129).to_a
end
def to_2025_row
[
overrides[:organisation_id] || log.owning_organisation&.old_visible_id, # 1
overrides[:managing_organisation_id] || log.managing_organisation&.old_visible_id,
log.assigned_to&.email,
log.needstype,
log.scheme&.id ? "S#{log.scheme&.id}" : "",
log.location&.id,
renewal,
log.startdate&.day,
log.startdate&.month,
log.startdate&.strftime("%y"), # 10
rent_type,
log.irproduct_other,
log.tenancycode,
log.propcode,
log.declaration,
log.rsnvac,
log.unitletas,
log.uprn,
log.address_line1&.tr(",", " "),
log.address_line2&.tr(",", " "), # 20
log.town_or_city&.tr(",", " "),
log.county&.tr(",", " "),
((log.postcode_full || "").split(" ") || [""]).first,
((log.postcode_full || "").split(" ") || [""]).last,
log.la,
log.unittype_gn,
log.builtype,
log.wchair,
log.beds,
log.voiddate&.day, # 30
log.voiddate&.month,
log.voiddate&.strftime("%y"),
log.mrcdate&.day,
log.mrcdate&.month,
log.mrcdate&.strftime("%y"),
log.sheltered,
log.joint,
log.startertenancy,
log.tenancy,
log.tenancyother, # 40
log.tenancylength,
log.age1 || overrides[:age1],
log.sex1,
log.ethnic,
log.nationality_all_group,
log.ecstat1,
relat_number(log.relat2),
log.age2 || overrides[:age2],
log.sex2,
log.ecstat2, # 50
relat_number(log.relat3),
log.age3 || overrides[:age3],
log.sex3,
log.ecstat3,
relat_number(log.relat4),
log.age4 || overrides[:age4],
log.sex4,
log.ecstat4,
relat_number(log.relat5),
log.age5 || overrides[:age5], # 60
log.sex5,
log.ecstat5,
relat_number(log.relat6),
log.age6 || overrides[:age6],
log.sex6,
log.ecstat6,
relat_number(log.relat7),
log.age7 || overrides[:age7],
log.sex7,
log.ecstat7, # 70
relat_number(log.relat8),
log.age8 || overrides[:age8],
log.sex8,
log.ecstat8,
log.armedforces,
log.leftreg,
log.reservist,
log.preg_occ,
log.housingneeds_a,
log.housingneeds_b, # 80
log.housingneeds_c,
log.housingneeds_f,
log.housingneeds_g,
log.housingneeds_h,
overrides[:illness] || log.illness,
log.illness_type_1,
log.illness_type_2,
log.illness_type_3,
log.illness_type_4,
log.illness_type_5, # 90
log.illness_type_6,
log.illness_type_7,
log.illness_type_8,
log.illness_type_9,
log.illness_type_10,
log.layear,
log.waityear,
log.reason,
log.reasonother,
log.prevten, # 100
homeless,
previous_postcode_known,
((log.ppostcode_full || "").split(" ") || [""]).first,
((log.ppostcode_full || "").split(" ") || [""]).last,
log.prevloc,
log.reasonpref,
log.rp_homeless,
log.rp_insan_unsat,
log.rp_medwel,
log.rp_hardship, # 110
log.rp_dontknow,
cbl,
chr,
cap,
accessible_register,
log.referral,
net_income_known,
log.incfreq,
log.earnings,
log.hb, # 120
log.benefits,
log.household_charge,
log.period,
log.brent,
log.scharge,
log.pscharge,
log.supcharg,
log.hbrentshortfall,
log.tshortfall, # 129
]
end
def to_2024_row
[
overrides[:organisation_id] || log.owning_organisation&.old_visible_id, # 1
@ -551,4 +643,15 @@ private
log.hhregres
end
end
def relat_number(value)
case value
when "P"
1
when "R"
3
when "C", "X"
2
end
end
end

139
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
when 2023, 2024, 2025
to_year_csv_row(year, seed:)
else
raise NotImplementedError "No mapping function implemented for year #{year}"
@ -67,6 +67,8 @@ class BulkUpload::SalesLogToCsv
[6, 3, 4, 5, nil, 28, 30, 38, 47, 51, 55, 59, 31, 39, 48, 52, 56, 60, 37, 46, 50, 54, 58, 35, 43, 49, 53, 57, 61, 32, 33, 78, 80, 79, 81, 83, 84, nil, 62, 66, 64, 65, 63, 67, 69, 70, 68, 76, 77, 16, 17, 18, 26, 24, 25, 27, 8, 91, 95, 96, 97, 92, 93, 94, 98, 100, 101, 103, 104, 106, 110, 111, 112, 113, 114, 9, 116, 117, 118, 120, 124, 125, 126, 10, 11, nil, 127, 129, 133, 134, 135, 1, 2, nil, 73, nil, 75, 107, 108, 121, 122, 130, 131, 82, 109, 123, 132, 115, 15, 86, 87, 29, 7, 12, 13, 14, 36, 44, 45, 88, 89, 102, 105, 119, 128, 19, 20, 21, 22, 23, 34, 40, 41, 42, 71, 72, 74, 85, 90, 99]
when 2024
(1..131).to_a
when 2025
(1..121).to_a
else
raise NotImplementedError "No mapping function implemented for year #{year}"
end
@ -395,6 +397,141 @@ class BulkUpload::SalesLogToCsv
]
end
def to_2025_row
[
log.saledate&.day,
log.saledate&.month,
log.saledate&.strftime("%y"),
overrides[:organisation_id] || log.owning_organisation&.old_visible_id,
overrides[:managing_organisation_id] || log.managing_organisation&.old_visible_id,
log.assigned_to&.email,
log.purchid,
log.ownershipsch,
log.ownershipsch == 1 ? log.type : "", # field_9: "What is the type of shared ownership sale?",
log.staircase, # 10
log.ownershipsch == 2 ? log.type : "", # field_11: "What is the type of discounted ownership sale?",
log.jointpur,
log.jointmore,
log.noint,
log.privacynotice,
log.uprn,
log.address_line1&.tr(",", " "), # 20
log.address_line2&.tr(",", " "),
log.town_or_city&.tr(",", " "),
log.county&.tr(",", " "),
((log.postcode_full || "").split(" ") || [""]).first,
((log.postcode_full || "").split(" ") || [""]).last,
log.la,
log.proptype,
log.beds,
log.builtype,
log.wchair,
log.age1,
log.sex1,
log.ethnic, # 30
log.nationality_all_group,
log.ecstat1,
log.buy1livein,
log.relat2,
log.age2,
log.sex2,
log.ethnic_group2,
log.nationality_all_buyer2_group,
log.ecstat2,
log.buy2livein, # 40
log.hholdcount,
log.relat3,
log.age3,
log.sex3,
log.ecstat3,
log.relat4,
log.age4,
log.sex4,
log.ecstat4,
log.relat5, # 50
log.age5,
log.sex5,
log.ecstat5,
log.relat6,
log.age6,
log.sex6,
log.ecstat6,
log.prevten,
log.ppcodenk,
((log.ppostcode_full || "").split(" ") || [""]).first, # 60
((log.ppostcode_full || "").split(" ") || [""]).last,
log.prevloc,
log.buy2living,
log.prevtenbuy2,
log.hhregres,
log.hhregresstill,
log.armedforcesspouse,
log.disabled,
log.wheel,
log.income1, # 70
log.inc1mort,
log.income2,
log.inc2mort,
log.hb,
log.savings.present? || "R",
log.prevown,
log.prevshared,
log.resale,
log.proplen,
log.hodate&.day, # 80
log.hodate&.month,
log.hodate&.strftime("%y"),
log.frombeds,
log.fromprop,
log.socprevten,
log.value,
log.equity,
log.mortgageused,
log.mortgage,
log.mortlen, # 90
log.deposit,
log.cashdis,
log.mrent,
log.mscharge,
log.management_fee,
log.stairbought,
log.stairowned,
log.staircasesale,
log.firststair,
log.initialpurchase&.day, # 100
log.initialpurchase&.month,
log.initialpurchase&.strftime("%y"),
log.numstair,
log.lasttransaction&.day,
log.lasttransaction&.month,
log.lasttransaction&.strftime("%y"),
log.value,
log.equity,
log.mortgageused,
log.mrentprestaircasing, # 110
log.mrent,
log.proplen,
log.value,
log.grant,
log.discount,
log.mortgageused,
log.mortgage,
log.mortlen,
log.extrabor,
log.deposit, # 120
log.mscharge,
]
end
def custom_field_numbers_row(seed: nil, field_numbers: nil)
if seed
["Field number"] + field_numbers.shuffle(random: Random.new(seed))

5
app/helpers/collection_deadline_helper.rb

@ -61,12 +61,15 @@ module CollectionDeadlineHelper
first_quarter(year).merge(quarter: "Q1"),
second_quarter(year).merge(quarter: "Q2"),
third_quarter(year).merge(quarter: "Q3"),
fourth_quarter(year).merge(quarter: "Q4"),
]
end
def quarter_for_date(date: Time.zone.now)
quarters = quarter_dates(current_collection_start_year)
collection_start_year = collection_start_year_for_date(date)
return unless QUARTERLY_DEADLINES.key?(collection_start_year)
quarters = quarter_dates(collection_start_year)
quarter = quarters.find { |q| date.between?(q[:start_date], q[:cutoff_date] + 1.day) }
return unless quarter

17
app/helpers/tasklist_helper.rb

@ -2,6 +2,7 @@ module TasklistHelper
include GovukLinkHelper
include GovukVisuallyHiddenHelper
include CollectionTimeHelper
include CollectionDeadlineHelper
def breadcrumb_logs_title(log, current_user)
log_type = log.lettings? ? "Lettings" : "Sales"
@ -70,6 +71,22 @@ module TasklistHelper
status == :cannot_start_yet ? "" : "govuk-task-list__item--with-link"
end
def deadline_text(log)
return if log.completed?
return if log.startdate.nil?
log_quarter = quarter_for_date(date: log.startdate)
return if log_quarter.nil?
deadline_for_log = log_quarter.cutoff_date
if deadline_for_log.beginning_of_day >= Time.zone.today.beginning_of_day
"<p class=\"govuk-body\">Upcoming #{log_quarter.quarter} deadline: #{log_quarter.cutoff_date.strftime('%-d %B %Y')}.<p>".html_safe
else
"<p class=\"govuk-body app-red-text\"><strong>Overdue: #{log_quarter.quarter} deadline #{log_quarter.cutoff_date.strftime('%-d %B %Y')}.</strong></p>".html_safe
end
end
private
def breadcrumb_organisation(log)

2
app/models/bulk_upload.rb

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

9
app/models/derived_variables/lettings_log_variables.rb

@ -72,6 +72,7 @@ module DerivedVariables::LettingsLogVariables
self.beds = 1
end
clear_child_ecstat_for_age_changes!
child_under_16_constraints!
self.hhtype = household_type
@ -258,6 +259,14 @@ private
end
end
def clear_child_ecstat_for_age_changes!
(2..8).each do |idx|
if public_send("age#{idx}_changed?") && self["ecstat#{idx}"] == 9
self["ecstat#{idx}"] = nil
end
end
end
def household_type
return unless totelder && totadult && totchild

10
app/models/derived_variables/sales_log_variables.rb

@ -46,6 +46,7 @@ module DerivedVariables::SalesLogVariables
if saledate && form.start_year_2024_or_later?
self.soctenant = soctenant_from_prevten_values
clear_child_ecstat_for_age_changes!
child_under_16_constraints!
end
@ -183,6 +184,15 @@ private
end
end
def clear_child_ecstat_for_age_changes!
start_index = joint_purchase? ? 3 : 2
(start_index..6).each do |idx|
if public_send("age#{idx}_changed?") && self["ecstat#{idx}"] == 9
self["ecstat#{idx}"] = nil
end
end
end
def household_type
return unless total_elder && total_adult && totchild

2
app/models/form/sales/pages/living_before_purchase.rb

@ -24,7 +24,7 @@ class Form::Sales::Pages::LivingBeforePurchase < ::Form::Page
end
def page_routed_to?(log)
return false if form.start_year_2025_or_later? && log.resale != 2
return false if form.start_year_2025_or_later? && log.resale != 2 && log.ownershipsch == 1
if @joint_purchase
log.joint_purchase?

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

@ -9,7 +9,7 @@ class Form::Sales::Questions::LivingBeforePurchaseYears < ::Form::Question
@step = 1
@width = 5
@ownershipsch = ownershipsch
@question_number = question_number
@question_number = QUESTION_NUMBER_FROM_YEAR_AND_OWNERSHIP.fetch(form.start_date.year, QUESTION_NUMBER_FROM_YEAR_AND_OWNERSHIP.max_by { |k, _v| k }.last)[ownershipsch]
end
def suffix_label(log)

9
app/models/forms/bulk_upload_form/prepare_your_file.rb

@ -10,10 +10,7 @@ module Forms
attribute :organisation_id, :integer
def view_path
case year
when 2024
"bulk_upload_#{log_type}_logs/forms/prepare_your_file_2024"
end
"bulk_upload_#{log_type}_logs/forms/prepare_your_file"
end
def back_path
@ -42,6 +39,10 @@ module Forms
"#{year} to #{year + 1}"
end
def slash_year_combo
"#{year}/#{(year + 1) % 100}"
end
def save!
true
end

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

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

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

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

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

@ -0,0 +1,122 @@
require "csv"
class BulkUpload::Lettings::Year2025::CsvParser
include CollectionTimeHelper
FIELDS = 129
MAX_COLUMNS = 130
FORM_YEAR = 2025
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::Year2025::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

1654
app/services/bulk_upload/lettings/year2025/row_parser.rb

File diff suppressed because it is too large Load Diff

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

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

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

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

1
app/services/bulk_upload/sales/year2024/row_parser.rb

@ -375,6 +375,7 @@ class BulkUpload::Sales::Year2024::RowParser
greater_than_or_equal_to: 0,
less_than_or_equal_to: 70,
if: :discounted_ownership?,
allow_blank: true,
},
on: :before_log

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

@ -0,0 +1,124 @@
require "csv"
class BulkUpload::Sales::Year2025::CsvParser
include CollectionTimeHelper
FIELDS = 121
MAX_COLUMNS = 142
FORM_YEAR = 2025
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::Year2025::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

1502
app/services/bulk_upload/sales/year2025/row_parser.rb

File diff suppressed because it is too large Load Diff

4
app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb → app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb

@ -14,7 +14,7 @@
<h2 class="govuk-heading-s">Download template</h2>
<p class="govuk-body govuk-!-margin-bottom-2"><%= govuk_link_to "Download the lettings bulk upload template (2024 to 2025)", @form.template_path %></p>
<p class="govuk-body govuk-!-margin-bottom-2"><%= govuk_link_to "Download the lettings bulk upload template (#{@form.year_combo})", @form.template_path %></p>
<p class="govuk-body govuk-!-margin-bottom-2">There are 8 rows of content in the templates. These rows are called the ‘headers’. They contain the CORE form questions and guidance about which questions are required and how to format your answers.</p>
<h2 class="govuk-heading-s">Create your file</h2>
@ -22,7 +22,7 @@
<%= govuk_list [
"Fill in the template with data from your housing management system. Your data should go below the headers, with one row per log. Leave column A blank - the bulk upload fields start in column B.",
"Make sure each column of your data aligns with the matching headers above. You may need to reorder your data.",
"Use the #{govuk_link_to 'Lettings bulk upload Specification (2024 to 2025)', @form.specification_path} to check your data is in the correct format.".html_safe,
"Use the #{govuk_link_to "Lettings bulk upload Specification (#{@form.year_combo})", @form.specification_path} to check your data is in the correct format.".html_safe,
"<strong>Username field:</strong> To assign a log to someone else, enter the email address they use to log into CORE.".html_safe,
"If you have reordered the headers, keep the headers in the file.",
], type: :bullet %>

6
app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb → app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb

@ -14,15 +14,15 @@
<h2 class="govuk-heading-s">Download template</h2>
<p class="govuk-body govuk-!-margin-bottom-2">Use one of these templates to upload logs for 2024/25:</p>
<p class="govuk-body govuk-!-margin-bottom-2"><%= govuk_link_to "Download the sales bulk upload template (2024 to 2025)", @form.template_path %>: In this template, the questions are in the same order as the 2024/25 paper form and web form.</p>
<p class="govuk-body govuk-!-margin-bottom-2">Use one of these templates to upload logs for <%= @form.slash_year_combo %>:</p>
<p class="govuk-body govuk-!-margin-bottom-2"><%= govuk_link_to "Download the sales bulk upload template (#{@form.year_combo})", @form.template_path %>: In this template, the questions are in the same order as the <%= @form.slash_year_combo %> paper form and web form.</p>
<p class="govuk-body govuk-!-margin-bottom-2">There are 8 rows of content in the templates. These rows are called the ‘headers’. They contain the CORE form questions and guidance about which questions are required and how to format your answers.</p>
<h2 class="govuk-heading-s">Create your file</h2>
<%= govuk_list [
"Fill in the template with data from your housing management system. Your data should go below the headers, with one row per log. The bulk upload fields start at column B. Leave column A blank.",
"Make sure each column of your data aligns with the matching headers above. You may need to reorder your data.",
"Use the #{govuk_link_to 'Sales bulk upload Specification (2024 to 2025)', @form.specification_path} to check your data is in the correct format.".html_safe,
"Use the #{govuk_link_to "Sales bulk upload Specification (#{@form.year_combo})", @form.specification_path} to check your data is in the correct format.".html_safe,
"<strong>Username field:</strong> To assign a log to someone else, enter the email address they use to log into CORE.".html_safe,
"If you have reordered the headers, keep the headers in the file.",
], type: :bullet %>

2
app/views/logs/edit.html.erb

@ -25,6 +25,7 @@
<% end %>
</p>
<% elsif @log.status == "not_started" %>
<p class="govuk-body"><%= govuk_link_to "Guidance for submitting social housing lettings and sales data (opens in a new tab)", guidance_path, target: "#" %></p>
<p class="govuk-body">This log has not been started.</p>
<% elsif @log.status == "completed" %>
<p class="govuk-body">
@ -36,6 +37,7 @@
</p>
<% end %>
<%= deadline_text(@log) %>
<%= render "tasklist" %>
<%= edit_actions_for_log(@log, bulk_upload_filter_applied) %>

2
config/locales/en.yml

@ -57,6 +57,8 @@ en:
<<: *bulk_upload__row_parser__base
bulk_upload/lettings/year2023/row_parser:
<<: *bulk_upload__row_parser__base
bulk_upload/sales/year2025/row_parser:
<<: *bulk_upload__row_parser__base
bulk_upload/sales/year2024/row_parser:
<<: *bulk_upload__row_parser__base
bulk_upload/sales/year2023/row_parser:

2
config/locales/forms/2025/sales/sale_information.en.yml

@ -148,7 +148,7 @@ en:
value_shared_ownership:
check_answer_label: "Full purchase price"
check_answer_prompt: ""
hint_text: "Enter the full purchase price of the property before any discounts are applied. For shared ownership, enter the full purchase price paid for 100% equity (this is equal to the value of the share owned by the PRP plus the value bought by the purchaser)."
hint_text: "Enter the full purchase price of the property before any discounts are applied. This is the full purchase price paid for 100% equity (this is equal to the value of the share owned by the PRP plus the value bought by the purchaser)."
question_text: "What was the full purchase price?"
value_shared_ownership_staircase:
check_answer_label: "Full purchase price"

60
config/locales/validations/lettings/2025/bulk_upload.en.yml

@ -0,0 +1,60 @@
en:
validations:
lettings:
2025:
bulk_upload:
not_answered: "You must answer %{question}"
invalid_option: "Enter a valid value for %{question}"
invalid_number: "Enter a number for %{question}"
spreadsheet_dupe: "This is a duplicate of a log in your file."
duplicate: "This is a duplicate log."
blank_file: "Template is blank - The template must be filled in for us to create the logs and check if data is correct."
wrong_template:
wrong_template: "Incorrect start dates, please ensure you have used the correct template."
no_headers: "Your file does not contain the required header rows. Add or check the header rows and upload your file again. [Read more about using the template headers](%{guidance_link})."
wrong_field_numbers_count: "Incorrect number of fields, please ensure you have used the correct template."
over_max_column_count: "Too many columns, please ensure you have used the correct template."
owning_organisation:
not_found: "The owning organisation code is incorrect."
not_stock_owner: "The owning organisation code provided is for an organisation that does not own stock."
not_permitted:
not_support: "You do not have permission to add logs for this owning organisation."
support: "This owning organisation is not affiliated with %{org_name}."
managing_organisation:
no_relationship: "This managing organisation does not have a relationship with the owning organisation."
not_found: "The managing organisation code is incorrect."
assigned_to:
not_found: "User with the specified email could not be found."
organisation_not_related: "User must be related to owning organisation or managing organisation."
startdate:
outside_collection_window: "Enter a date within the %{year_combo} collection year, which is between 1st April %{start_year} and 31st March %{end_year}."
year_not_two_or_four_digits: "Tenancy start year must be 2 or 4 digits."
housingneeds:
no_and_dont_know_disabled_needs_conjunction: "No disabled access needs and don’t know disabled access needs cannot be selected together."
dont_know_disabled_needs_conjunction: "Don’t know disabled access needs can’t be selected if you have selected fully wheelchair-accessible housing, wheelchair access to essential rooms, level access housing or other disabled access needs."
no_disabled_needs_conjunction: "No disabled access needs can’t be selected if you have selected fully wheelchair-accessible housing, wheelchair access to essential rooms, level access housing or other disabled access needs."
housingneeds_type:
only_one_option_permitted: "Only one disabled access need: fully wheelchair-accessible housing, wheelchair access to essential rooms or level access housing, can be selected."
condition_effects:
no_choices: "You cannot answer this question as you told us nobody in the household has a physical or mental health condition (or other illness) expected to last 12 months or more."
reason:
renewal_reason_needed: "The reason for leaving must be \"End of social or private sector tenancy - no fault\", \"End of social or private sector tenancy - evicted due to anti-social behaviour (ASB)\", \"End of social or private sector tenancy - evicted due to rent arrears\" or \"End of social or private sector tenancy - evicted for any other reason\"."
referral:
general_needs_prp_referred_by_la: "The source of the referral cannot be referred by local authority housing department for a general needs log."
nominated_by_local_ha_but_la: "The source of the referral cannot be Nominated by local housing authority as your organisation is a local authority."
scheme:
must_relate_to_org: "This scheme code does not belong to the owning organisation or managing organisation."
location:
must_relate_to_org: "Location code must relate to a location that is owned by the owning organisation or managing organisation."
age:
invalid: "Age of person %{person_num} must be a number or the letter R"
address:
not_found: "We could not find this address. Check the address data in your CSV file is correct and complete, or find the correct address in the service."
not_determined:
one: "There is a possible match for this address which doesn't look right. Check the address data in your CSV file is correct and complete, or confirm the address in the service."
multiple: "There are multiple matches for this address. Check the address data in your CSV file is correct and complete, or select the correct address in the service."
not_answered: "Enter either the UPRN or the full address."
nationality:
invalid: "Select a valid nationality."
charges:
missing_charges: "Please enter the %{sentence_fragment}. If there is no %{sentence_fragment}, please enter '0'."

46
config/locales/validations/sales/2025/bulk_upload.en.yml

@ -0,0 +1,46 @@
en:
validations:
sales:
2025:
bulk_upload:
not_answered: "You must answer %{question}"
invalid_option: "Enter a valid value for %{question}"
spreadsheet_dupe: "This is a duplicate of a log in your file."
duplicate: "This is a duplicate log."
blank_file: "Template is blank - The template must be filled in for us to create the logs and check if data is correct."
wrong_template:
over_max_column_count: "Too many columns, please ensure you have used the correct template."
no_headers: "Your file does not contain the required header rows. Add or check the header rows and upload your file again. [Read more about using the template headers](%{guidance_link})."
wrong_field_numbers_count: "Incorrect number of fields, please ensure you have used the correct template."
wrong_template: "Incorrect sale dates, please ensure you have used the correct template."
numeric:
within_range: "%{field} must be between %{min} and %{max}."
owning_organisation:
not_found: "The owning organisation code is incorrect."
not_stock_owner: "The owning organisation code provided is for an organisation that does not own stock."
not_permitted:
support: "This owning organisation is not affiliated with %{name}."
not_support: "You do not have permission to add logs for this owning organisation."
assigned_to:
not_found: "User with the specified email could not be found."
organisation_not_related: "User must be related to owning organisation or managing organisation."
managing_organisation_not_related: "This organisation does not have a relationship with the owning organisation."
saledate:
outside_collection_window: "Enter a date within the %{year_combo} collection year, which is between 1st April %{start_year} and 31st March %{end_year}."
year_not_two_or_four_digits: "Sale completion year must be 2 or 4 digits."
ecstat1:
buyer_cannot_be_over_16_and_child: "Buyer 1's age cannot be 16 or over if their working situation is child under 16."
buyer_cannot_be_child: "Buyer 1 cannot have a working situation of child under 16."
age1:
buyer_cannot_be_over_16_and_child: "Buyer 1's age cannot be 16 or over if their working situation is child under 16."
ecstat2:
buyer_cannot_be_over_16_and_child: "Buyer 2's age cannot be 16 or over if their working situation is child under 16."
buyer_cannot_be_child: "Buyer 2 cannot have a working situation of child under 16."
age2:
buyer_cannot_be_over_16_and_child: "Buyer 2's age cannot be 16 or over if their working situation is child under 16."
address:
not_found: "We could not find this address. Check the address data in your CSV file is correct and complete, or select the correct address using the CORE site."
not_determined: "There are multiple matches for this address. Either select the correct address manually or correct the UPRN in the CSV file."
not_answered: "Enter either the UPRN or the full address."
nationality:
invalid: "Select a valid nationality."

2
config/routes.rb

@ -404,8 +404,10 @@ Rails.application.routes.draw do
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-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"
end
end

BIN
session-manager-plugin.deb

Binary file not shown.

45
spec/helpers/tasklist_helper_spec.rb

@ -204,4 +204,49 @@ RSpec.describe TasklistHelper do
end
end
end
describe "deadline text" do
context "when log does not have a sale/start date" do
let(:log) { build(:sales_log, saledate: nil) }
it "returns nil" do
expect(deadline_text(log)).to be_nil
end
end
context "when log is completed" do
let(:log) { build(:sales_log, :completed, status: "completed") }
it "returns nil" do
expect(deadline_text(log)).to be_nil
end
end
context "when today is before the deadline for log with sale/start date" do
let(:log) { build(:sales_log, saledate: Time.zone.local(2025, 6, 1)) }
it "returns the deadline text" do
allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 5, 7))
expect(deadline_text(log)).to include("Upcoming Q1 deadline: 11 July 2025.")
end
end
context "when today is the deadline for log with sale/start date" do
let(:log) { build(:sales_log, saledate: Time.zone.local(2025, 2, 1)) }
it "returns the overdue text" do
allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 6, 6))
expect(deadline_text(log)).to include("Upcoming Q4 deadline: 6 June 2025.")
end
end
context "when today is after the deadline for log with sale/start date" do
let(:log) { build(:sales_log, saledate: Time.zone.local(2025, 2, 1)) }
it "returns the overdue text" do
allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 6, 7))
expect(deadline_text(log)).to include("Overdue: Q4 deadline 6 June 2025.")
end
end
end
end

1
spec/models/bulk_upload_spec.rb

@ -41,6 +41,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" },
].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]) }

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

@ -95,7 +95,7 @@ RSpec.describe Form::Sales::Pages::LivingBeforePurchase, type: :model do
end
it "does not route to the page when resale is not 2" do
log = build(:sales_log, jointpur: 1, resale: nil)
log = build(:sales_log, jointpur: 1, resale: nil, ownershipsch: 1)
expect(page.routed_to?(log, nil)).to eq(false)
end
end

20
spec/models/lettings_log_derived_fields_spec.rb

@ -1206,4 +1206,24 @@ RSpec.describe LettingsLog, type: :model do
end
end
end
describe "#clear_child_ecstat_for_age_changes!" do
it "clears the working situation of a person that was previously a child under 16" do
log = create(:lettings_log, :completed, age2: 13)
log.age2 = 17
expect { log.set_derived_fields! }.to change(log, :ecstat2).from(9).to(nil)
end
it "does not clear the working situation of a person that had an age change but is still a child under 16" do
log = create(:lettings_log, :completed, age2: 13)
log.age2 = 15
expect { log.set_derived_fields! }.to not_change(log, :ecstat2)
end
it "does not clear the working situation of a person that had an age change but is still an adult" do
log = create(:lettings_log, :completed, age2: 45)
log.age2 = 46
expect { log.set_derived_fields! }.to not_change(log, :ecstat2)
end
end
end

20
spec/models/sales_log_derived_fields_spec.rb

@ -78,6 +78,26 @@ RSpec.describe SalesLog, type: :model do
expect { log.set_derived_fields! }.to change(log, :mortgage).from(50_000).to(nil)
end
describe "#clear_child_ecstat_for_age_changes!" do
it "clears the working situation of a person that was previously a child under 16" do
log = create(:sales_log, :completed, age3: 13, age4: 16, age5: 45)
log.age3 = 17
expect { log.set_derived_fields! }.to change(log, :ecstat3).from(9).to(nil)
end
it "does not clear the working situation of a person that had an age change but is still a child under 16" do
log = create(:sales_log, :completed, age3: 13, age4: 16, age5: 45)
log.age3 = 15
expect { log.set_derived_fields! }.to not_change(log, :ecstat3)
end
it "does not clear the working situation of a person that had an age change but is still an adult" do
log = create(:sales_log, :completed, age3: 13, age4: 16, age5: 45)
log.age5 = 46
expect { log.set_derived_fields! }.to not_change(log, :ecstat5)
end
end
context "with a log that is not outright sales" do
it "does not derive deposit when mortgage used is no" do
log = build(:sales_log, :shared_ownership_setup_complete, value: 123_400, deposit: nil, mortgageused: 2)

15
spec/requests/lettings_logs_controller_spec.rb

@ -1155,6 +1155,21 @@ RSpec.describe LettingsLogsController, type: :request do
expect(lettings_log.status).to eq("completed")
expect(page).to have_link("review and make changes to this log", href: "/lettings-logs/#{lettings_log.id}/review")
end
it "does not show guidance link" do
expect(page).not_to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)")
end
end
context "and the log is not started" do
let(:lettings_log) { create(:lettings_log, status: "not_started", assigned_to: user) }
it "shows guidance link" do
allow(Time.zone).to receive(:now).and_return(lettings_log.form.edit_end_date - 1.day)
get lettings_log_path(lettings_log)
expect(lettings_log.status).to eq("not_started")
expect(page).to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)")
end
end
context "with bulk_upload_id filter" do

11
spec/requests/sales_logs_controller_spec.rb

@ -866,6 +866,7 @@ RSpec.describe SalesLogsController, type: :request do
context "when viewing a sales log" do
let(:headers) { { "Accept" => "text/html" } }
let(:completed_sales_log) { FactoryBot.create(:sales_log, :completed, owning_organisation: user.organisation, assigned_to: user) }
let(:not_started_sales_log) { FactoryBot.create(:sales_log, owning_organisation: user.organisation, assigned_to: user) }
before do
sign_in user
@ -956,6 +957,16 @@ RSpec.describe SalesLogsController, type: :request do
expect(page).to have_content("This log is from the 2021 to 2022 collection window, which is now closed.")
end
end
it "does not show guidance link" do
get "/sales-logs/#{completed_sales_log.id}", headers:, params: {}
expect(page).not_to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)")
end
it "shows guidance link for not_started log" do
get "/sales-logs/#{not_started_sales_log.id}", headers:, params: {}
expect(page).to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)")
end
end
context "when requesting CSV download" do

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

@ -103,7 +103,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
before do
values = log_to_csv.to_2024_row
values[7] = nil
file.write(log_to_csv.default_2024_field_numbers_row)
file.write(log_to_csv.default_field_numbers_row_for_year(2024))
file.write(log_to_csv.to_custom_csv_row(seed: nil, field_values: values))
file.rewind
end
@ -146,7 +146,7 @@ RSpec.describe BulkUpload::Lettings::Validator do
before do
log.needstype = nil
values = log_to_csv.to_2024_row
file.write(log_to_csv.default_2024_field_numbers_row(seed:))
file.write(log_to_csv.default_field_numbers_row_for_year(2024, seed:))
file.write(log_to_csv.to_custom_csv_row(seed:, field_values: values))
file.close
end

32
spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb

@ -15,8 +15,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
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_2023_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023))
file.rewind
end
@ -39,8 +39,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
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_2023_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023))
file.rewind
end
@ -64,8 +64,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
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_2023_field_numbers_row(seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row(seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023, seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023, seed:))
file.rewind
end
@ -108,7 +108,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
context "when parsing csv without headers" do
before do
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2023_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2023))
file.rewind
end
@ -127,7 +127,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
before do
file.write(bom)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2023_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2023))
file.rewind
end
@ -141,7 +141,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
before do
file.write(invalid_sequence)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2023_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2023))
file.rewind
end
@ -158,8 +158,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
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_2023_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023))
file.rewind
end
@ -177,8 +177,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
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_2023_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023))
file.rewind
end
@ -190,7 +190,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
context "when without headers using default ordering" do
before do
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2023_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2023))
file.rewind
end
@ -210,8 +210,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do
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_2023_field_numbers_row(seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row(seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023, seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023, seed:))
file.rewind
end

36
spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb

@ -15,8 +15,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
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_2024_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024))
file.rewind
end
@ -38,8 +38,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
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_2024_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024))
file.rewind
end
@ -62,8 +62,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
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_2024_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024))
file.write("\n")
file.rewind
end
@ -92,8 +92,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
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_2024_field_numbers_row(seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row(seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024, seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024, seed:))
file.rewind
end
@ -136,7 +136,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
context "when parsing csv without headers" do
before do
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2024))
file.rewind
end
@ -155,7 +155,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
before do
file.write(bom)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2024))
file.rewind
end
@ -169,7 +169,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
before do
file.write(invalid_sequence)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2024))
file.rewind
end
@ -186,8 +186,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
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_2024_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024))
file.rewind
end
@ -205,8 +205,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
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_2024_field_numbers_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024))
file.rewind
end
@ -218,7 +218,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
context "when without headers using default ordering" do
before do
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row)
file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2024))
file.rewind
end
@ -238,8 +238,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do
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_2024_field_numbers_row(seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row(seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024, seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024, seed:))
file.rewind
end

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

@ -0,0 +1,254 @@
require "rails_helper"
RSpec.describe BulkUpload::Lettings::Year2025::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(2025))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025))
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(2025))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025))
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(2025))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025))
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(2025, seed:))
file.write(BulkUpload::LettingsLogToCsv.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_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_2025_field_numbers + %w[invalid_field_number] }
let(:field_values) { log_to_csv.to_2025_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(2025))
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(2025))
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(2025))
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(2025))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025))
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(2025))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025))
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(2025))
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(2025, seed:))
file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025, 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

2808
spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb

File diff suppressed because it is too large Load Diff

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

@ -5,162 +5,187 @@ RSpec.describe BulkUpload::Sales::LogCreator do
let(:owning_org) { create(:organisation, old_visible_id: 123) }
let(:user) { create(:user, organisation: owning_org) }
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) }
let(:log) { build(:sales_log, :completed, assigned_to: user, owning_organisation: owning_org, managing_organisation: owning_org) }
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)
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
[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) }
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,
)
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)
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
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
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
end
context "when valid csv with existing log" do
xit "what should happen?"
# 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)
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
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 "does not set unanswered soft validations" do
service.call
it "associates log with bulk upload" 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)
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 "when valid csv with existing log" do
xit "what should happen?"
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
end

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

@ -58,6 +58,22 @@ RSpec.describe BulkUpload::Sales::Validator do
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) }
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.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")])
end
end
[
{ line_ending: "\n", name: "unix" },
{ line_ending: "\r\n", name: "windows" },

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

@ -0,0 +1,191 @@
require "rails_helper"
RSpec.describe BulkUpload::Sales::Year2025::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(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 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(2025))
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(2025))
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(2025))
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(2025))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025))
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(2025))
file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025))
file.rewind
end
it "parses csv correctly" do
expect(service.row_parsers[0].field_16).to eql(log.uprn)
end
end
end

1915
spec/services/bulk_upload/sales/year2025/row_parser_spec.rb

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