Browse Source

Merge branch 'main' into CLDC-3843-Lettings-household-situation-change-question-format

pull/2958/head
Manny Dinssa 2 months ago committed by GitHub
parent
commit
d90c5d0dde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 0
      .rake_tasks~
  2. 11
      app/components/create_log_actions_component.html.erb
  3. 4
      app/components/create_log_actions_component.rb
  4. 70
      app/controllers/address_search_controller.rb
  5. 34
      app/controllers/test_data_controller.rb
  6. 73
      app/frontend/controllers/address_search_controller.js
  7. 3
      app/frontend/controllers/index.js
  8. 4
      app/frontend/styles/_task-list.scss
  9. 253
      app/helpers/bulk_upload/lettings_log_to_csv.rb
  10. 139
      app/helpers/bulk_upload/sales_log_to_csv.rb
  11. 5
      app/helpers/collection_deadline_helper.rb
  12. 17
      app/helpers/tasklist_helper.rb
  13. 2
      app/models/bulk_upload.rb
  14. 24
      app/models/derived_variables/lettings_log_variables.rb
  15. 59
      app/models/derived_variables/sales_log_variables.rb
  16. 9
      app/models/form/lettings/pages/address_fallback.rb
  17. 17
      app/models/form/lettings/pages/address_search.rb
  18. 44
      app/models/form/lettings/questions/address_search.rb
  19. 1
      app/models/form/lettings/questions/postcode_for_full_address.rb
  20. 6
      app/models/form/lettings/subsections/property_information.rb
  21. 11
      app/models/form/sales/pages/address_fallback.rb
  22. 23
      app/models/form/sales/pages/address_search.rb
  23. 2
      app/models/form/sales/pages/living_before_purchase.rb
  24. 1
      app/models/form/sales/pages/no_address_found.rb
  25. 44
      app/models/form/sales/questions/address_search.rb
  26. 2
      app/models/form/sales/questions/living_before_purchase_years.rb
  27. 1
      app/models/form/sales/questions/postcode_for_full_address.rb
  28. 6
      app/models/form/sales/subsections/property_information.rb
  29. 9
      app/models/forms/bulk_upload_form/prepare_your_file.rb
  30. 18
      app/models/log.rb
  31. 7
      app/services/address_client.rb
  32. 2
      app/services/bulk_upload/lettings/log_creator.rb
  33. 2
      app/services/bulk_upload/lettings/validator.rb
  34. 13
      app/services/bulk_upload/lettings/year2024/row_parser.rb
  35. 122
      app/services/bulk_upload/lettings/year2025/csv_parser.rb
  36. 1667
      app/services/bulk_upload/lettings/year2025/row_parser.rb
  37. 2
      app/services/bulk_upload/sales/log_creator.rb
  38. 2
      app/services/bulk_upload/sales/validator.rb
  39. 2
      app/services/bulk_upload/sales/year2024/row_parser.rb
  40. 124
      app/services/bulk_upload/sales/year2025/csv_parser.rb
  41. 1503
      app/services/bulk_upload/sales/year2025/row_parser.rb
  42. 22
      app/services/csv/lettings_log_csv_service.rb
  43. 14
      app/services/csv/sales_log_csv_service.rb
  44. 8
      app/services/uprn_data_presenter.rb
  45. 4
      app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb
  46. 6
      app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb
  47. 24
      app/views/form/_address_search_question.html.erb
  48. 5
      app/views/form/_select_question.html.erb
  49. 3
      app/views/form/guidance/_address_fallback.html.erb
  50. 7
      app/views/form/guidance/_address_search.html.erb
  51. 2
      app/views/logs/edit.html.erb
  52. 2
      config/locales/en.yml
  53. 7
      config/locales/forms/2024/lettings/guidance.en.yml
  54. 7
      config/locales/forms/2024/lettings/property_information.en.yml
  55. 7
      config/locales/forms/2024/sales/guidance.en.yml
  56. 7
      config/locales/forms/2024/sales/property_information.en.yml
  57. 7
      config/locales/forms/2025/lettings/guidance.en.yml
  58. 45
      config/locales/forms/2025/lettings/property_information.en.yml
  59. 7
      config/locales/forms/2025/sales/guidance.en.yml
  60. 45
      config/locales/forms/2025/sales/property_information.en.yml
  61. 2
      config/locales/forms/2025/sales/sale_information.en.yml
  62. 60
      config/locales/validations/lettings/2025/bulk_upload.en.yml
  63. 46
      config/locales/validations/sales/2025/bulk_upload.en.yml
  64. 14
      config/locales/validations/sales/property_information.en.yml
  65. 6
      config/routes.rb
  66. 6
      db/migrate/20250219122817_add_manual_address_entry_selected_to_logs.rb
  67. 2
      db/schema.rb
  68. 15
      lib/tasks/correct_reasonpref_values.rake
  69. BIN
      session-manager-plugin.deb
  70. 9
      spec/factories/lettings_log.rb
  71. 10
      spec/factories/sales_log.rb
  72. 45
      spec/features/form/address_search_spec.rb
  73. 60
      spec/features/form/form_navigation_spec.rb
  74. 375
      spec/features/lettings_log_spec.rb
  75. 375
      spec/features/sales_log_spec.rb
  76. 2
      spec/fixtures/files/sales_logs_csv_export_codes_24.csv
  77. 2
      spec/fixtures/files/sales_logs_csv_export_labels_24.csv
  78. 2
      spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv
  79. 45
      spec/helpers/tasklist_helper_spec.rb
  80. 111
      spec/lib/tasks/correct_reasonpref_values_spec.rb
  81. 5
      spec/models/bulk_upload_spec.rb
  82. 9
      spec/models/form/lettings/pages/address_fallback_spec.rb
  83. 42
      spec/models/form/lettings/pages/address_search_spec.rb
  84. 68
      spec/models/form/lettings/questions/address_search_spec.rb
  85. 8
      spec/models/form/lettings/questions/uprn_selection_spec.rb
  86. 4
      spec/models/form/lettings/questions/uprn_spec.rb
  87. 12
      spec/models/form/lettings/subsections/property_information_spec.rb
  88. 9
      spec/models/form/sales/pages/address_fallback_spec.rb
  89. 42
      spec/models/form/sales/pages/address_search_spec.rb
  90. 2
      spec/models/form/sales/pages/living_before_purchase_spec.rb
  91. 4
      spec/models/form/sales/pages/uprn_confirmation_spec.rb
  92. 68
      spec/models/form/sales/questions/address_search_spec.rb
  93. 8
      spec/models/form/sales/questions/uprn_selection_spec.rb
  94. 12
      spec/models/form/sales/subsections/property_information_spec.rb
  95. 20
      spec/models/lettings_log_derived_fields_spec.rb
  96. 20
      spec/models/sales_log_derived_fields_spec.rb
  97. 7
      spec/models/sales_log_spec.rb
  98. 4
      spec/models/validations/sales/property_validations_spec.rb
  99. 2
      spec/request_helper.rb
  100. 148
      spec/requests/address_search_controller_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

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

70
app/controllers/address_search_controller.rb

@ -0,0 +1,70 @@
class AddressSearchController < ApplicationController
before_action :authenticate_user!
before_action :set_log, only: %i[manual_input search_input]
def index
query = params[:query]
if query.match?(/\A\d+\z/) && query.length > 5
# Query is all numbers and greater than 5 digits, assume it's a UPRN
service = UprnClient.new(query)
service.call
if service.error.present?
render json: { error: service.error }, status: :not_found
else
presenter = UprnDataPresenter.new(service.result)
render json: [{ text: presenter.address, value: presenter.uprn }]
end
elsif query.match?(/[a-zA-Z]/)
# Query contains letters, assume it's an address
service = AddressClient.new(query, { minmatch: 0.2 })
service.call
if service.error.present?
render json: { error: service.error }, status: :not_found
else
results = service.result.map do |result|
presenter = AddressDataPresenter.new(result)
{ text: presenter.address, value: presenter.uprn }
end
render json: results
end
else
# Query is ambiguous, use both APIs and merge results
address_service = AddressClient.new(query, { minmatch: 0.2 })
uprn_service = UprnClient.new(query)
address_service.call
uprn_service.call
results = ([uprn_service.result] || []) + (address_service.result || [])
if address_service.error.present? && uprn_service.error.present?
render json: { error: "Address and UPRN are not recognised." }, status: :not_found
else
formatted_results = results.map do |result|
presenter = AddressDataPresenter.new(result)
{ text: presenter.address, value: presenter.uprn }
end
render json: formatted_results
end
end
end
def manual_input
@log.update!(manual_address_entry_selected: true)
redirect_to polymorphic_url([@log, :address])
end
def search_input
@log.update!(manual_address_entry_selected: false)
redirect_to polymorphic_url([@log, :address_search])
end
private
def set_log
@log = current_user.send("#{params[:log_type]}s").find(params[:log_id])
end
end

34
app/controllers/test_data_controller.rb

@ -4,65 +4,71 @@ class TestDataController < ApplicationController
def create_test_lettings_log
return render_not_found unless FeatureToggle.create_test_logs_enabled?
log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA")
log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA", manual_address_entry_selected: false)
redirect_to lettings_log_path(log)
end
def create_setup_test_lettings_log
return render_not_found unless FeatureToggle.create_test_logs_enabled?
log = FactoryBot.create(:lettings_log, :setup_completed, assigned_to: current_user)
log = FactoryBot.create(:lettings_log, :setup_completed, assigned_to: current_user, manual_address_entry_selected: false)
redirect_to lettings_log_path(log)
end
def create_2024_test_lettings_bulk_upload
%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")
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: "test_lettings_log.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?
log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user)
log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, manual_address_entry_selected: false)
redirect_to sales_log_path(log)
end
def create_setup_test_sales_log
return render_not_found unless FeatureToggle.create_test_logs_enabled?
log = FactoryBot.create(:sales_log, :shared_ownership_setup_complete, assigned_to: current_user)
log = FactoryBot.create(:sales_log, :shared_ownership_setup_complete, assigned_to: current_user, manual_address_entry_selected: false)
redirect_to sales_log_path(log)
end
def create_2024_test_sales_bulk_upload
[2024, 2025].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("test_sales_log.csv")
log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, value: 180_000, deposit: 150_000)
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: "test_sales_log.csv",
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

73
app/frontend/controllers/address_search_controller.js

@ -0,0 +1,73 @@
import { Controller } from '@hotwired/stimulus'
import accessibleAutocomplete from 'accessible-autocomplete'
import 'accessible-autocomplete/dist/accessible-autocomplete.min.css'
const options = []
const fetchOptions = async (query, searchUrl) => {
if (query.length < 2) {
throw new Error('Query must be at least 2 characters long.')
}
try {
const response = await fetch(`${searchUrl}?query=${encodeURIComponent(query.trim())}`)
return await response.json()
} catch (error) {
return error
}
}
const fetchAndPopulateSearchResults = async (query, populateResults, searchUrl, populateOptions, selectEl) => {
if (/\S/.test(query)) {
try {
const results = await fetchOptions(query, searchUrl)
if (results.length === 0) {
populateOptions([], selectEl)
populateResults([])
} else {
populateOptions(results, selectEl)
populateResults(Object.values(results).map((o) => `${o.text} (${o.value})`))
}
} catch (error) {
populateOptions([], selectEl)
populateResults([])
}
}
}
const populateOptions = (results, selectEl) => {
selectEl.innerHTML = ''
results.forEach((result) => {
const option = document.createElement('option')
option.value = result.value
option.innerHTML = `${result.text} (${result.value})`
selectEl.appendChild(option)
options.push(option)
})
}
export default class extends Controller {
connect () {
const searchUrl = JSON.parse(this.element.dataset.info).search_url
const selectEl = this.element
accessibleAutocomplete.enhanceSelectElement({
defaultValue: '',
selectElement: selectEl,
minLength: 2,
source: (query, populateResults) => {
fetchAndPopulateSearchResults(query, populateResults, searchUrl, populateOptions, selectEl)
},
autoselect: true,
showNoOptionsFound: true,
placeholder: 'Start typing to search',
templates: { suggestion: (value) => value },
onConfirm: (val) => {
const selectedResult = Array.from(selectEl.options).find(option => option.text === val)
if (selectedResult) {
selectedResult.selected = true
}
}
})
}
}

3
app/frontend/controllers/index.js

@ -19,6 +19,8 @@ import FilterLayoutController from './filter_layout_controller.js'
import TabsController from './tabs_controller.js'
import AddressSearchController from './address_search_controller.js'
application.register('accessible-autocomplete', AccessibleAutocompleteController)
application.register('conditional-filter', ConditionalFilterController)
application.register('conditional-question', ConditionalQuestionController)
@ -27,3 +29,4 @@ application.register('numeric-question', NumericQuestionController)
application.register('filter-layout', FilterLayoutController)
application.register('search', SearchController)
application.register('tabs', TabsController)
application.register('address-search', AddressSearchController)

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

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

253
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
send("to_#{year}_row")
rescue NoMethodError
raise NotImplementedError "No mapping function implemented for year #{year}"
end
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
default_field_numbers_row_for_year(year, seed:)
rescue NoMethodError
raise NotImplementedError "No mapping function implemented for year #{year}"
end
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
send("default_#{year}_field_numbers")
rescue NoMethodError
raise NotImplementedError "No mapping function implemented for year #{year}"
end
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

24
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
@ -112,6 +113,21 @@ module DerivedVariables::LettingsLogVariables
self.previous_la_known = nil if is_renewal?
end
if form.start_year_2024_or_later?
if manual_address_entry_selected
self.uprn_known = 0
self.uprn_selection = nil
self.uprn_confirmed = nil
else
self.uprn_confirmed = 1 if uprn.present?
self.uprn_known = 1 if uprn.present?
reset_address_fields! if uprn.blank?
if uprn_changed?
self.uprn_selection = uprn
end
end
end
if is_renewal?
self.underoccupation_benefitcap = 2 if collection_start_year == 2021
self.voiddate = startdate
@ -249,6 +265,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

59
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
@ -54,27 +55,29 @@ module DerivedVariables::SalesLogVariables
if uprn_known&.zero?
self.uprn = nil
if uprn_known_was == 1
self.address_line1 = nil
self.address_line2 = nil
self.town_or_city = nil
self.county = nil
self.pcodenk = nil
self.postcode_full = nil
self.la = nil
reset_address_fields!
end
end
if uprn_known == 1 && uprn_confirmed&.zero?
self.uprn = nil
reset_address_fields!
self.uprn_known = 0
self.uprn_confirmed = nil
self.address_line1 = nil
self.address_line2 = nil
self.town_or_city = nil
self.county = nil
self.pcodenk = nil
self.postcode_full = nil
self.la = nil
end
if form.start_year_2024_or_later?
if manual_address_entry_selected
self.uprn_known = 0
self.uprn_selection = nil
self.uprn_confirmed = nil
else
self.uprn_confirmed = 1 if uprn.present?
self.uprn_known = 1 if uprn.present?
reset_address_fields! if uprn.blank?
if uprn_changed?
self.uprn_selection = uprn
end
end
end
if form.start_year_2025_or_later? && is_bedsit?
@ -181,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
@ -238,4 +250,21 @@ private
def prevten_was_social_housing?
[1, 2].include?(prevten) || [1, 2].include?(prevtenbuy2)
end
def reset_address_fields!
self.uprn = nil
self.uprn_known = nil
self.address_line1 = nil
self.address_line2 = nil
self.town_or_city = nil
self.county = nil
self.pcode1 = nil
self.pcode2 = nil
self.pcodenk = nil
self.address_line1_input = nil
self.postcode_full_input = nil
self.postcode_full = nil
self.is_la_inferred = nil
self.la = nil
end
end

9
app/models/form/lettings/pages/address_fallback.rb

@ -3,14 +3,7 @@ class Form::Lettings::Pages::AddressFallback < ::Form::Page
super
@id = "address"
@copy_key = "lettings.property_information.address"
@depends_on = [
{ "is_supported_housing?" => false, "uprn_known" => nil, "uprn_selection" => "uprn_not_listed" },
{ "is_supported_housing?" => false, "uprn_known" => 0, "uprn_selection" => "uprn_not_listed" },
{ "is_supported_housing?" => false, "uprn_confirmed" => 0, "uprn_selection" => "uprn_not_listed" },
{ "is_supported_housing?" => false, "uprn_known" => nil, "address_options_present?" => false },
{ "is_supported_housing?" => false, "uprn_known" => 0, "address_options_present?" => false },
{ "is_supported_housing?" => false, "uprn_confirmed" => 0, "address_options_present?" => false },
]
@depends_on = [{ "is_supported_housing?" => false, "manual_address_entry_selected" => true }]
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end

17
app/models/form/lettings/pages/address_search.rb

@ -0,0 +1,17 @@
class Form::Lettings::Pages::AddressSearch < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "address_search"
@copy_key = "sales.property_information.address_search"
@depends_on = [{ "is_supported_housing?" => false, "manual_address_entry_selected" => false }]
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
def questions
@questions ||= [
Form::Lettings::Questions::AddressSearch.new(nil, nil, self),
]
end
QUESTION_NUMBER_FROM_YEAR = { 2024 => 12, 2025 => 16 }.freeze
end

44
app/models/form/lettings/questions/address_search.rb

@ -0,0 +1,44 @@
class Form::Lettings::Questions::AddressSearch < ::Form::Question
def initialize(id, hsh, page)
super
@id = "uprn"
@type = "address_search"
@copy_key = "lettings.property_information.address_search"
@plain_label = true
@bottom_guidance_partial = "address_search"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
@hide_question_number_on_page = true
end
def answer_options(log = nil, _user = nil)
return {} unless ActiveRecord::Base.connected?
return {} unless log&.address_options&.any?
log.address_options.each_with_object({}) do |option, hash|
hash[option[:uprn]] = { "value" => "#{option[:address]} (#{option[:uprn]})" }
end
end
def get_extra_check_answer_value(log)
return unless log.uprn_known == 1
value = [
log.address_line1,
log.address_line2,
log.town_or_city,
log.county,
log.postcode_full,
(LocalAuthority.find_by(code: log.la)&.name if log.la.present?),
].select(&:present?)
return unless value.any?
"\n\n#{value.join("\n")}"
end
def displayed_answer_options(log, user = nil)
answer_options(log, user).transform_values { |value| value["value"] } || {}
end
QUESTION_NUMBER_FROM_YEAR = { 2024 => 12, 2025 => 16 }.freeze
end

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

@ -20,6 +20,7 @@ class Form::Lettings::Questions::PostcodeForFullAddress < ::Form::Question
@disable_clearing_if_not_routed_or_dynamic_answer_options = true
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
@hide_question_number_on_page = true
@bottom_guidance_partial = "address_fallback"
end
QUESTION_NUMBER_FROM_YEAR = { 2023 => 12, 2024 => 13, 2025 => 17 }.freeze

6
app/models/form/lettings/subsections/property_information.rb

@ -32,11 +32,7 @@ class Form::Lettings::Subsections::PropertyInformation < ::Form::Subsection
def uprn_questions
if form.start_year_2024_or_later?
[
Form::Lettings::Pages::Uprn.new(nil, nil, self),
Form::Lettings::Pages::UprnConfirmation.new(nil, nil, self),
Form::Lettings::Pages::AddressMatcher.new(nil, nil, self),
Form::Lettings::Pages::NoAddressFound.new(nil, nil, self), # soft validation
Form::Lettings::Pages::UprnSelection.new(nil, nil, self),
Form::Lettings::Pages::AddressSearch.new(nil, nil, self),
Form::Lettings::Pages::AddressFallback.new(nil, nil, self),
]
else

11
app/models/form/sales/pages/address_fallback.rb

@ -3,14 +3,7 @@ class Form::Sales::Pages::AddressFallback < ::Form::Page
super
@id = "address"
@copy_key = "sales.property_information.address"
@depends_on = [
{ "uprn_known" => nil, "uprn_selection" => "uprn_not_listed" },
{ "uprn_known" => 0, "uprn_selection" => "uprn_not_listed" },
{ "uprn_confirmed" => 0, "uprn_selection" => "uprn_not_listed" },
{ "uprn_known" => nil, "address_options_present?" => false },
{ "uprn_known" => 0, "address_options_present?" => false },
{ "uprn_confirmed" => 0, "address_options_present?" => false },
]
@depends_on = [{ "manual_address_entry_selected" => true }]
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
@ -24,5 +17,5 @@ class Form::Sales::Pages::AddressFallback < ::Form::Page
]
end
QUESTION_NUMBER_FROM_YEAR = { 2024 => 16, 2025 => 16 }.freeze
QUESTION_NUMBER_FROM_YEAR = { 2024 => 16, 2025 => 14 }.freeze
end

23
app/models/form/sales/pages/address_search.rb

@ -0,0 +1,23 @@
class Form::Sales::Pages::AddressSearch < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "address_search"
@copy_key = "sales.property_information.address_search"
@depends_on = [{ "manual_address_entry_selected" => false }]
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
end
def questions
@questions ||= [
Form::Sales::Questions::AddressSearch.new(nil, nil, self),
]
end
def skip_href(log = nil)
return unless log
"/#{log.log_type.dasherize}s/#{log.id}/property-number-of-bedrooms"
end
QUESTION_NUMBER_FROM_YEAR = { 2024 => 15, 2025 => 13 }.freeze
end

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?

1
app/models/form/sales/pages/no_address_found.rb

@ -16,7 +16,6 @@ class Form::Sales::Pages::NoAddressFound < ::Form::Page
{ "uprn_known" => nil, "address_options_present?" => false },
{ "uprn_known" => 0, "address_options_present?" => false },
{ "uprn_confirmed" => 0, "address_options_present?" => false },
]
end

44
app/models/form/sales/questions/address_search.rb

@ -0,0 +1,44 @@
class Form::Sales::Questions::AddressSearch < ::Form::Question
def initialize(id, hsh, page)
super
@id = "uprn"
@type = "address_search"
@copy_key = "sales.property_information.address_search"
@plain_label = true
@bottom_guidance_partial = "address_search"
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
@hide_question_number_on_page = true
end
def answer_options(log = nil, _user = nil)
return {} unless ActiveRecord::Base.connected?
return {} unless log&.address_options&.any?
log.address_options.each_with_object({}) do |option, hash|
hash[option[:uprn]] = { "value" => "#{option[:address]} (#{option[:uprn]})" }
end
end
def get_extra_check_answer_value(log)
return unless log.uprn_known == 1
value = [
log.address_line1,
log.address_line2,
log.town_or_city,
log.county,
log.postcode_full,
(LocalAuthority.find_by(code: log.la)&.name if log.la.present?),
].select(&:present?)
return unless value.any?
"\n\n#{value.join("\n")}"
end
def displayed_answer_options(log, user = nil)
answer_options(log, user).transform_values { |value| value["value"] } || {}
end
QUESTION_NUMBER_FROM_YEAR = { 2024 => 15, 2025 => 13 }.freeze
end

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)

1
app/models/form/sales/questions/postcode_for_full_address.rb

@ -20,6 +20,7 @@ class Form::Sales::Questions::PostcodeForFullAddress < ::Form::Question
@disable_clearing_if_not_routed_or_dynamic_answer_options = true
@question_number = QUESTION_NUMBER_FROM_YEAR[form.start_date.year] || QUESTION_NUMBER_FROM_YEAR[QUESTION_NUMBER_FROM_YEAR.keys.max]
@hide_question_number_on_page = true
@bottom_guidance_partial = "address_fallback"
end
QUESTION_NUMBER_FROM_YEAR = { 2023 => 15, 2024 => 16, 2025 => 14 }.freeze

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

@ -24,11 +24,7 @@ class Form::Sales::Subsections::PropertyInformation < ::Form::Subsection
def uprn_questions
if form.start_year_2024_or_later?
[
Form::Sales::Pages::Uprn.new(nil, nil, self),
Form::Sales::Pages::UprnConfirmation.new(nil, nil, self),
Form::Sales::Pages::AddressMatcher.new(nil, nil, self),
Form::Sales::Pages::NoAddressFound.new(nil, nil, self),
Form::Sales::Pages::UprnSelection.new(nil, nil, self),
Form::Sales::Pages::AddressSearch.new(nil, nil, self),
Form::Sales::Pages::AddressFallback.new(nil, nil, self),
Form::Sales::Pages::PropertyLocalAuthority.new(nil, nil, self),
Form::Sales::Pages::Buyer1IncomeDiscountedMaxValueCheck.new("local_authority_buyer_1_income_max_value_check", nil, self, check_answers_card_number: nil),

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

18
app/models/log.rb

@ -75,8 +75,7 @@ class Log < ApplicationRecord
presenter = UprnDataPresenter.new(service.result)
self.uprn_known = 1
self.uprn_confirmed = nil unless skip_update_uprn_confirmed
self.uprn_selection = nil
self.uprn_selection = uprn
self.address_line1 = presenter.address_line1
self.address_line2 = presenter.address_line2
self.town_or_city = presenter.town_or_city
@ -126,16 +125,27 @@ class Log < ApplicationRecord
end
def address_options
if uprn.present?
service = UprnClient.new(uprn)
service.call
if service.result.blank? || service.error.present?
@address_options = []
return @address_options
end
presenter = UprnDataPresenter.new(service.result)
@address_options = [{ address: presenter.address, uprn: presenter.uprn }]
else
return @address_options if @address_options && @last_searched_address_string == address_string
return if address_string.blank?
if [address_line1_input, postcode_full_input].all?(&:present?)
@last_searched_address_string = address_string
service = AddressClient.new(address_string)
service.call
if service.result.blank? || service.error.present?
@address_options = []
return @answer_options
return @address_options
end
address_opts = []

7
app/services/address_client.rb

@ -7,8 +7,9 @@ class AddressClient
ADDRESS = "api.os.uk".freeze
PATH = "/search/places/v1/find".freeze
def initialize(address)
def initialize(address, options = {})
@address = address
@options = options
end
def call
@ -43,8 +44,8 @@ private
params = {
query: address,
key: ENV["OS_DATA_KEY"],
maxresults: 10,
minmatch: 0.4,
maxresults: @options[:maxresults] || 10,
minmatch: @options[:minmatch] || 0.4,
}
uri.query = URI.encode_www_form(params)
uri.to_s

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

13
app/services/bulk_upload/lettings/year2024/row_parser.rb

@ -444,6 +444,7 @@ class BulkUpload::Lettings::Year2024::RowParser
validate :validate_incomplete_soft_validations, on: :after_log
validate :validate_nationality, on: :after_log
validate :validate_reasonpref_reason_values, on: :after_log
validate :validate_nulls, on: :after_log
@ -677,6 +678,17 @@ private
end
end
def validate_reasonpref_reason_values
valid_reasonpref_reason_options = %w[0 1]
%w[field_107 field_108 field_109 field_110 field_111].each do |field|
next unless send(field).present? && !valid_reasonpref_reason_options.include?(send(field).to_s)
question_text = QUESTIONS[field.to_sym]
question_text[0] = question_text[0].downcase
errors.add(field.to_sym, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: question_text))
end
end
def duplicate_check_fields
[
"startdate",
@ -1341,6 +1353,7 @@ private
attributes["address_line1_input"] = address_line1_input
attributes["postcode_full_input"] = postcode_full
attributes["select_best_address_match"] = true if field_16.blank?
attributes["manual_address_entry_selected"] = field_16.blank?
end
attributes

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

1667
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

2
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
@ -985,6 +986,7 @@ private
attributes["address_line1_input"] = address_line1_input
attributes["postcode_full_input"] = postcode_full
attributes["select_best_address_match"] = true if field_22.blank?
attributes["manual_address_entry_selected"] = field_22.blank?
attributes["ethnic_group2"] = infer_buyer2_ethnic_group_from_ethnic
attributes["ethnicbuy2"] = field_40

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

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

File diff suppressed because it is too large Load Diff

22
app/services/csv/lettings_log_csv_service.rb

@ -208,6 +208,11 @@ module Csv
3 => "Intermediate Rent",
}.freeze
UPRN_KNOWN_LABELS = {
0 => "No",
1 => "Yes",
}.freeze
LABELS = {
"lettype" => LETTYPE_LABELS,
"irproduct" => IRPRODUCT_LABELS,
@ -215,6 +220,7 @@ module Csv
"newprop" => NEWPROP_LABELS,
"incref" => INCREF_LABELS,
"renttype" => RENTTYPE_LABELS,
"uprn_known" => UPRN_KNOWN_LABELS,
}.freeze
CONVENTIONAL_YES_NO_ATTRIBUTES = %w[illness_type_1 illness_type_2 illness_type_3 illness_type_4 illness_type_5 illness_type_6 illness_type_7 illness_type_8 illness_type_9 illness_type_10 refused cbl cap chr accessible_register letting_allocation_none housingneeds_a housingneeds_b housingneeds_c housingneeds_d housingneeds_e housingneeds_f housingneeds_g housingneeds_h has_benefits nocharge postcode_known].freeze
@ -249,6 +255,18 @@ module Csv
"letting_allocation_unknown" => %w[letting_allocation_none],
}.freeze
ATTRIBUTE_MAPPINGS_2024 = {
"uprn" => %w[uprn_known uprn],
}.freeze
def attribute_mappings
if @year >= 2024
ATTRIBUTE_MAPPINGS.merge(ATTRIBUTE_MAPPINGS_2024)
else
ATTRIBUTE_MAPPINGS
end
end
ORDERED_ADDRESS_FIELDS = %w[uprn address_line1 address_line2 town_or_city county postcode_full is_la_inferred la_label la uprn_known uprn_selection address_search_value_check address_line1_input postcode_full_input address_line1_as_entered address_line2_as_entered town_or_city_as_entered county_as_entered postcode_full_as_entered la_as_entered].freeze
SUPPORT_ONLY_ATTRIBUTES = %w[postcode_known is_la_inferred totchild totelder totadult net_income_known previous_la_known is_previous_la_inferred age1_known age2_known age3_known age4_known age5_known age6_known age7_known age8_known details_known_2 details_known_3 details_known_4 details_known_5 details_known_6 details_known_7 details_known_8 wrent wscharge wpschrge wsupchrg wtcharge wtshortfall old_form_id old_id tshortfall_known hhtype la prevloc updated_by_id uprn_confirmed address_line1_input postcode_full_input uprn_selection address_line1_as_entered address_line2_as_entered town_or_city_as_entered county_as_entered postcode_full_as_entered la_as_entered created_by].freeze
@ -279,10 +297,10 @@ module Csv
ordered_questions.flat_map do |question|
if question.type == "checkbox"
question.answer_options.keys.reject { |key| key == "divider" }.map { |key|
ATTRIBUTE_MAPPINGS.fetch(key, key)
attribute_mappings.fetch(key, key)
}.flatten
else
ATTRIBUTE_MAPPINGS.fetch(question.id, question.id)
attribute_mappings.fetch(question.id, question.id)
end
end
end

14
app/services/csv/sales_log_csv_service.rb

@ -152,6 +152,15 @@ module Csv
"uprn_confirmed" => "UPRNCONFIRMED",
}.freeze
UPRN_CONFIRMED_LABELS = {
0 => "No",
1 => "Yes",
}.freeze
LABELS = {
"uprn_confirmed" => UPRN_CONFIRMED_LABELS,
}.freeze
def formatted_attribute_headers
return @attributes unless @user.support?
@ -208,6 +217,9 @@ module Csv
unless @user.support? && @year >= 2024
mappings["postcode_full"] = %w[pcode1 pcode2]
end
if @year >= 2024
mappings["uprn"] = %w[uprn uprn_confirmed address_line1_input postcode_full_input uprn_selection]
end
mappings
end
@ -280,6 +292,8 @@ module Csv
end
def get_label(value, attribute, log)
return LABELS[attribute][value] if LABELS.key?(attribute)
log.form
.get_question(attribute, log)
&.label_from_value(value)

8
app/services/uprn_data_presenter.rb

@ -55,4 +55,12 @@ class UprnDataPresenter
def result_from_lpi?
data["LPI_KEY"].present?
end
def uprn
data["UPRN"]
end
def address
data["ADDRESS"]
end
end

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 %>

24
app/views/form/_address_search_question.html.erb

@ -0,0 +1,24 @@
<% selected = @log.public_send(question.id) || "" %>
<% answers = question.displayed_answer_options(@log, current_user).map { |key, value| OpenStruct.new(id: key, name: select_option_name(value), resource: value) } %>
<%= render partial: "form/guidance/#{question.top_guidance_partial}" if question.top_guidance? %>
<%= f.govuk_select(question.id.to_sym,
label: legend(question, page_header, conditional),
"data-controller": "address-search",
"data-info": { search_url: address_search_url }.to_json,
caption: caption(caption_text, page_header, conditional),
hint: { text: question.hint_text&.html_safe }) do %>
<% if answers.any? %>
<% answers.each do |answer| %>
<option value="<%= answer.id %>"
data-synonyms="<%= answer_option_synonyms(answer.resource) %>"
data-append="<%= answer_option_append(answer.resource) %>"
data-hint="<%= answer_option_hint(answer.resource) %>"
<%= question.answer_selected?(@log, answer) ? "selected" : "" %>><%= answer.name || answer.resource %></option>
<% end %>
<% else %>
<option value="" disabled>Javascript is disabled. Please enter the address manually.</option>
<% end %>
<% end %>
<%= render partial: "form/guidance/#{question.bottom_guidance_partial}" if question.bottom_guidance? %>

5
app/views/form/_select_question.html.erb

@ -1,4 +1,3 @@
<% selected = @log.public_send(question.id) || "" %>
<% answers = question.displayed_answer_options(@log, current_user).map { |key, value| OpenStruct.new(id: key, name: select_option_name(value), resource: value) } %>
<%= render partial: "form/guidance/#{question.top_guidance_partial}" if question.top_guidance? %>
@ -8,6 +7,7 @@
"data-controller": "accessible-autocomplete",
caption: caption(caption_text, page_header, conditional),
hint: { text: question.hint_text&.html_safe }) do %>
<% if answers.any? %>
<% answers.each do |answer| %>
<option value="<%= answer.id %>"
data-synonyms="<%= answer_option_synonyms(answer.resource) %>"
@ -15,6 +15,9 @@
data-hint="<%= answer_option_hint(answer.resource) %>"
<%= question.answer_selected?(@log, answer) ? "selected" : "" %>><%= answer.name || answer.resource %></option>
<% end %>
<% else %>
<option value="" disabled></option>
<% end %>
<% end %>
<%= render partial: "form/guidance/#{question.bottom_guidance_partial}" if question.bottom_guidance? %>

3
app/views/form/guidance/_address_fallback.html.erb

@ -0,0 +1,3 @@
<div class="govuk-button-group">
<%= govuk_link_to "Clear address and search instead", address_search_input_path(@log.log_type, @log.id), class: "govuk-button govuk-button--secondary" %>
</div>

7
app/views/form/guidance/_address_search.html.erb

@ -0,0 +1,7 @@
<%= govuk_details(summary_text: I18n.t("forms.#{@log.form.start_date.year}.#{@log.form.type}.guidance.address_search.title")) do %>
<%= I18n.t("forms.#{@log.form.start_date.year}.#{@log.form.type}.guidance.address_search.content").html_safe %>
<% end %>
<div class="govuk-button-group">
<%= govuk_link_to "Enter the address manually instead", address_manual_input_path(@log.log_type, @log.id), class: "govuk-button govuk-button--secondary" %>
</div>

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:

7
config/locales/forms/2024/lettings/guidance.en.yml

@ -61,3 +61,10 @@ en:
<li>child benefit</li>
<li>council tax support</li>
</ul>"
address_search:
title: "Can’t find the address you’re looking for?"
content: "<ul class=\"govuk-list govuk-list--bullet\">
<li>Some properties may not be available yet e.g. new builds; you might need to enter them manually instead</li>
<li>For UPRN (Unique Property Reference Number), please enter the full value exactly</li>
</ul>"

7
config/locales/forms/2024/lettings/property_information.en.yml

@ -50,6 +50,13 @@ en:
hint_text: ""
question_text: "Select the correct address"
address_search:
page_header: "What is the property's address?"
check_answer_label: "Address"
check_answer_prompt: "Enter address or UPRN"
hint_text: "For example, '1 Victoria Road' or '10010457355'"
question_text: "Enter address or UPRN"
address:
page_header: "What is the property's address?"
address_line1:

7
config/locales/forms/2024/sales/guidance.en.yml

@ -44,3 +44,10 @@ en:
privacy_notice_buyer:
content: "Make sure the buyer has seen or been given access to %{privacy_notice_link} before completing this log. This is a legal requirement under data protection legislation."
privacy_notice_link_text: "the Ministry of Housing, Communities and Local Government (MHCLG) privacy notice"
address_search:
title: "Can’t find the address you’re looking for?"
content: "<ul class=\"govuk-list govuk-list--bullet\">
<li>Some properties may not be available yet e.g. new builds; you might need to enter them manually instead</li>
<li>For UPRN (Unique Property Reference Number), please enter the full value exactly</li>
</ul>"

7
config/locales/forms/2024/sales/property_information.en.yml

@ -43,6 +43,13 @@ en:
hint_text: ""
question_text: "Select the correct address"
address_search:
page_header: "What is the property's address?"
check_answer_label: "Address"
check_answer_prompt: "Enter address or UPRN"
hint_text: "For example, '1 Victoria Road' or '10010457355'"
question_text: "Enter address or UPRN"
address:
page_header: "What is the property's address?"
address_line1:

7
config/locales/forms/2025/lettings/guidance.en.yml

@ -61,3 +61,10 @@ en:
<li>child benefit</li>
<li>council tax support</li>
</ul>"
address_search:
title: "Can’t find the address you’re looking for?"
content: "<ul class=\"govuk-list govuk-list--bullet\">
<li>Some properties may not be available yet e.g. new builds; you might need to enter them manually instead</li>
<li>For UPRN (Unique Property Reference Number), please enter the full value exactly</li>
</ul>"

45
config/locales/forms/2025/lettings/property_information.en.yml

@ -10,45 +10,12 @@ en:
hint_text: ""
question_text: "Is this the first time the property has been let as social housing?"
uprn:
page_header: ""
uprn_known:
check_answer_label: "UPRN known"
check_answer_prompt: "Enter UPRN if known"
hint_text: "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.<br><br>The UPRN may not be the same as the property reference assigned by your organisation.<br><br>If you don’t know the UPRN you can enter the address of the property instead on the next screen."
question_text: "Do you know the property's UPRN?"
uprn:
check_answer_label: "UPRN"
check_answer_prompt: ""
hint_text: ""
question_text: "What is the property's UPRN?"
uprn_confirmed:
page_header: "We found an address that might be this property"
check_answer_label: "Is this the right address?"
check_answer_prompt: "Tell us if this is the right address"
hint_text: ""
question_text: "Is this the property address?"
address_matcher:
page_header: "Find an address"
address_line1_input:
check_answer_label: "Find address"
check_answer_prompt: "Try find address"
hint_text: ""
question_text: "Address line 1"
postcode_full_input:
check_answer_label: ""
check_answer_prompt: ""
hint_text: ""
question_text: "Postcode"
uprn_selection:
page_header: "We found an address that might be this property"
check_answer_label: "Select correct address"
check_answer_prompt: "Select correct address"
hint_text: ""
question_text: "Select the correct address"
address_search:
page_header: "What is the property's address?"
check_answer_label: "Address"
check_answer_prompt: "Enter address or UPRN"
hint_text: "For example, '1 Victoria Road' or '10010457355'"
question_text: "Enter address or UPRN"
address:
page_header: "What is the property's address?"

7
config/locales/forms/2025/sales/guidance.en.yml

@ -44,3 +44,10 @@ en:
privacy_notice_buyer:
content: "Make sure the buyer has seen or been given access to %{privacy_notice_link} before completing this log. This is a legal requirement under data protection legislation."
privacy_notice_link_text: "the Ministry of Housing, Communities and Local Government (MHCLG) privacy notice"
address_search:
title: "Can’t find the address you’re looking for?"
content: "<ul class=\"govuk-list govuk-list--bullet\">
<li>Some properties may not be available yet e.g. new builds; you might need to enter them manually instead</li>
<li>For UPRN (Unique Property Reference Number), please enter the full value exactly</li>
</ul>"

45
config/locales/forms/2025/sales/property_information.en.yml

@ -3,45 +3,12 @@ en:
2025:
sales:
property_information:
uprn:
page_header: ""
uprn_known:
check_answer_label: "UPRN known"
check_answer_prompt: "Enter UPRN if known"
hint_text: "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.<br><br>The UPRN may not be the same as the property reference assigned by your organisation.<br><br>If you don’t know the UPRN you can enter the address of the property instead on the next screen."
question_text: "Do you know the property's UPRN?"
uprn:
check_answer_label: "UPRN"
check_answer_prompt: ""
hint_text: ""
question_text: "What is the property's UPRN?"
uprn_confirmed:
page_header: "We found an address that might be this property"
check_answer_label: "Is this the right address?"
check_answer_prompt: "Tell us if this is the right address"
hint_text: ""
question_text: "Is this the property address?"
address_matcher:
page_header: "Find an address"
address_line1_input:
check_answer_label: "Find address"
check_answer_prompt: "Try find address"
hint_text: ""
question_text: "Address line 1"
postcode_full_input:
check_answer_label: ""
check_answer_prompt: ""
hint_text: ""
question_text: "Postcode"
uprn_selection:
page_header: "We found an address that might be this property"
check_answer_label: "Select correct address"
check_answer_prompt: "Select correct address"
hint_text: ""
question_text: "Select the correct address"
address_search:
page_header: "What is the property's address?"
check_answer_label: "Address"
check_answer_prompt: "Enter address or UPRN"
hint_text: "For example, '1 Victoria Road' or '10010457355'"
question_text: "Enter address or UPRN"
address:
page_header: "What is the property's address?"

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."

14
config/locales/validations/sales/property_information.en.yml

@ -7,7 +7,7 @@ en:
joint_purchase: "Buyers’ last accommodation and discounted ownership postcodes must match."
not_joint_purchase: "Buyer’s last accommodation and discounted ownership postcodes must match."
invalid: "Enter a postcode in the correct format, for example AA1 1AA."
not_in_england: "It looks like you have entered a postcode outside of England - only submit Lettings forms for Lettings that occur in England"
not_in_england: "It looks like you have entered a postcode outside of England. Only create logs for sales in England."
ppostcode_full:
postcode_must_match_previous:
joint_purchase: "Buyers’ last accommodation and discounted ownership postcodes must match."
@ -21,7 +21,7 @@ en:
joint_purchase: "Buyers’ last accommodation and discounted ownership postcodes must match."
not_joint_purchase: "Buyer’s last accommodation and discounted ownership postcodes must match."
invalid: "UPRN must be 12 digits or less."
not_in_england: "It looks like you have an entered a postcode outside of England. Only create logs for lettings in England."
not_in_england: "It looks like you have entered an address outside of England. Only create logs for sales in England."
beds:
bedsits_have_max_one_bedroom: "Number of bedrooms must be 1 if the property is a bedsit."
proptype:
@ -29,11 +29,11 @@ en:
uprn_known:
invalid: "You must answer UPRN known?"
la:
not_in_england: "It looks like you have entered an address outside of England. Only create logs for lettings in England."
not_in_england: "It looks like you have entered an address outside of England. Only create logs for sales in England."
uprn_confirmation:
not_in_england: "It looks like you have entered an address outside of England. Only create logs for lettings in England."
not_in_england: "It looks like you have entered an address outside of England. Only create logs for sales in England."
uprn_selection:
not_in_england: "It looks like you have entered an address outside of England. Only create logs for lettings in England."
not_in_england: "It looks like you have entered an address outside of England. Only create logs for sales in England."
saledate:
postcode_not_in_england: "It looks like you have an entered a postcode outside of England. Only create logs for lettings in England."
address_not_in_england: "It looks like you have entered an address outside of England. Only create logs for lettings in England."
postcode_not_in_england: "It looks like you have entered a postcode outside of England. Only create logs for sales in England."
address_not_in_england: "It looks like you have entered an address outside of England. Only create logs for sales in England."

6
config/routes.rb

@ -39,6 +39,10 @@ Rails.application.routes.draw do
get "/data-sharing-agreement", to: "content#data_sharing_agreement"
get "/service-moved", to: "maintenance#service_moved"
get "/service-unavailable", to: "maintenance#service_unavailable"
get "/address-search", to: "address_search#index"
get "/address-search/current", to: "address_search#current"
get "/address-search/manual-input/:log_type/:log_id", to: "address_search#manual_input", as: "address_manual_input"
get "/address-search/search-input/:log_type/:log_id", to: "address_search#search_input", as: "address_search_input"
get "collection-resources", to: "collection_resources#index"
get "/collection-resources/:log_type/:year/:resource_type/download", to: "collection_resources#download_mandatory_collection_resource", as: :download_mandatory_collection_resource
@ -400,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

6
db/migrate/20250219122817_add_manual_address_entry_selected_to_logs.rb

@ -0,0 +1,6 @@
class AddManualAddressEntrySelectedToLogs < ActiveRecord::Migration[7.2]
def change
add_column :sales_logs, :manual_address_entry_selected, :boolean, default: false
add_column :lettings_logs, :manual_address_entry_selected, :boolean, default: false
end
end

2
db/schema.rb

@ -374,6 +374,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_25_180643) do
t.integer "multiple_partners_value_check"
t.bigint "created_by_id"
t.integer "referral_type"
t.boolean "manual_address_entry_selected", default: false
t.index ["assigned_to_id"], name: "index_lettings_logs_on_assigned_to_id"
t.index ["bulk_upload_id"], name: "index_lettings_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_lettings_logs_on_created_by_id"
@ -770,6 +771,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_25_180643) do
t.decimal "mrentprestaircasing", precision: 10, scale: 2
t.datetime "lasttransaction"
t.datetime "initialpurchase"
t.boolean "manual_address_entry_selected", default: false
t.index ["assigned_to_id"], name: "index_sales_logs_on_assigned_to_id"
t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id"

15
lib/tasks/correct_reasonpref_values.rake

@ -0,0 +1,15 @@
desc "Correct invalid BU reasonable preference values"
task correct_reasonpref_values: :environment do
%w[rp_homeless rp_hardship rp_medwel rp_insan_unsat rp_dontknow].each do |field|
field_invalid = "#{field} != 1 AND #{field} != 0 AND #{field} is NOT NULL"
LettingsLog.filter_by_year(2024).where(field_invalid).find_each do |lettings_log|
lettings_log[field] = 0
unless lettings_log.save
Rails.logger.info("Failed to save reasonpref for LettingsLog with id #{lettings_log.id}: #{lettings_log.errors.full_messages}")
end
end
LettingsLog.filter_by_year(2023).where(field_invalid).update_all("#{field}": 0)
end
end

BIN
session-manager-plugin.deb

Binary file not shown.

9
spec/factories/lettings_log.rb

@ -6,6 +6,7 @@ FactoryBot.define do
managing_organisation { assigned_to.organisation }
created_at { Time.zone.today }
updated_at { Time.zone.today }
manual_address_entry_selected { true }
before(:create) do |log, _evaluator|
if log.period && !log.managing_organisation.organisation_rent_periods.exists?(rent_period: log.period)
@ -167,13 +168,11 @@ FactoryBot.define do
town_or_city { Faker::Address.city }
ppcodenk { 1 }
tshortfall_known { 1 }
after(:build) do |log, _evaluator|
after(:build) do |log, evaluator|
if log.startdate >= Time.zone.local(2024, 4, 1)
log.address_line1_input = log.address_line1
log.postcode_full_input = log.postcode_full
log.nationality_all_group = 826
log.uprn = "10033558653"
log.uprn_selection = 1
log.uprn = evaluator.uprn || "10033558653"
log.uprn_selection = evaluator.uprn_selection || "10033558653"
end
end
end

10
spec/factories/sales_log.rb

@ -8,6 +8,8 @@ FactoryBot.define do
managing_organisation { assigned_to.organisation }
created_at { Time.zone.now }
updated_at { Time.zone.now }
manual_address_entry_selected { true }
trait :in_progress do
purchid { "PC123" }
ownershipsch { 2 }
@ -166,14 +168,12 @@ FactoryBot.define do
nationalbuy2 { 13 }
buy2living { 3 }
proplen_asked { 1 }
after(:build) do |log, _evaluator|
after(:build) do |log, evaluator|
if log.saledate >= Time.zone.local(2024, 4, 1)
log.address_line1_input = log.address_line1
log.postcode_full_input = log.postcode_full
log.nationality_all_group = 826
log.nationality_all_buyer2_group = 826
log.uprn = "10033558653"
log.uprn_selection = 1
log.uprn = evaluator.uprn || "10033558653"
log.uprn_selection = evaluator.uprn_selection || "10033558653"
end
if log.saledate >= Time.zone.local(2025, 4, 1)
log.relat2 = "X" if log.relat2 == "C"

45
spec/features/form/address_search_spec.rb

@ -0,0 +1,45 @@
require "rails_helper"
require_relative "helpers"
RSpec.describe "Address Search" do
include Helpers
let(:user) { FactoryBot.create(:user) }
let(:sales_log) do
FactoryBot.create(
:sales_log,
:shared_ownership_setup_complete,
assigned_to: user,
manual_address_entry_selected: false,
)
end
before do
sign_in user
end
context "when using address search feature" do
before do
visit("/sales-logs/#{sales_log.id}/address-search")
end
it "allows searching by a UPRN", js: true do
find("#sales-log-uprn-field").click.native.send_keys("1", "0", "0", "3", "3", "5", "4", "4", "6", "1", "4", :down)
expect(find("#sales-log-uprn-field").value).to eq("10033544614")
end
it "allows searching by address", js: true do
find("#sales-log-uprn-field").click.native.send_keys("S", "W", "1", "5", :down, :enter)
expect(find("#sales-log-uprn-field").value).to eq("SW15")
end
it "displays the placeholder text", js: true do
expect(find("#sales-log-uprn-field")["placeholder"]).to eq("Start typing to search")
end
it "displays correct bottom guidance text" do
find("span.govuk-details__summary-text", text: "Can’t find the address you’re looking for?").click
expect(page).to have_content("Some properties may not be available yet e.g. new builds; you might need to enter them manually instead")
expect(page).to have_content("For UPRN (Unique Property Reference Number), please enter the full value exactly")
end
end
end

60
spec/features/form/form_navigation_spec.rb

@ -186,64 +186,4 @@ RSpec.describe "Form Navigation" do
expect(page).to have_current_path("/lettings-logs/#{id}/duplicate-logs?original_log_id=#{id}")
end
end
describe "searching for an address" do
let(:now) { Time.zone.local(2024, 5, 1) }
context "with a lettings log" do
let(:lettings_log) { create(:lettings_log, :setup_completed, startdate: Time.zone.local(2024, 5, 5), assigned_to: user) }
before do
stub_request(:get, /api\.os\.uk/)
.to_return(status: 200, body: { results: [{ DPA: { MATCH: 0.9, BUILDING_NAME: "result address line 1", POST_TOWN: "result town or city", POSTCODE: "AA1 1AA", UPRN: "12345" } }] }.to_json, headers: {})
end
it "allows searching for an address" do
visit("lettings-logs/#{id}/address-matcher")
fill_in("lettings-log-address-line1-input-field", with: "address")
fill_in("lettings-log-postcode-full-input-field", with: "A1 1AA")
click_button(text: "Search")
expect(page).to have_current_path("/lettings-logs/#{id}/uprn-selection")
end
it "allows searching for an address from check your answers page" do
visit("lettings-logs/#{id}/address-matcher?referrer=check_answers")
fill_in("lettings-log-address-line1-input-field", with: "address")
fill_in("lettings-log-postcode-full-input-field", with: "A1 1AA")
click_button(text: "Search")
expect(page).to have_current_path("/lettings-logs/#{id}/uprn-selection?referrer=check_answers&unanswered_pages=property_local_authority")
choose("lettings-log-uprn-selection-12345-field", allow_label_click: true)
click_button(text: "Save changes")
expect(page).to have_current_path("/lettings-logs/#{id}/property-information/check-answers")
end
end
context "with a sales log" do
let(:sales_log) { create(:sales_log, :outright_sale_setup_complete, saledate: Time.zone.local(2024, 5, 5), assigned_to: user) }
before do
stub_request(:get, /api\.os\.uk/)
.to_return(status: 200, body: { results: [{ DPA: { MATCH: 0.9, BUILDING_NAME: "result address line 1", POST_TOWN: "result town or city", POSTCODE: "AA1 1AA", UPRN: "12345" } }] }.to_json, headers: {})
end
it "allows searching for an address" do
visit("sales-logs/#{sales_log.id}/address-matcher")
fill_in("sales-log-address-line1-input-field", with: "address")
fill_in("sales-log-postcode-full-input-field", with: "A1 1AA")
click_button(text: "Search")
expect(page).to have_current_path("/sales-logs/#{sales_log.id}/uprn-selection")
end
it "allows searching for an address from check your answers page" do
visit("sales-logs/#{sales_log.id}/address-matcher?referrer=check_answers")
fill_in("sales-log-address-line1-input-field", with: "address")
fill_in("sales-log-postcode-full-input-field", with: "A1 1AA")
click_button(text: "Search")
expect(page).to have_current_path("/sales-logs/#{sales_log.id}/uprn-selection?referrer=check_answers&unanswered_pages=property_local_authority")
choose("sales-log-uprn-selection-12345-field", allow_label_click: true)
click_button(text: "Save changes")
expect(page).to have_current_path("/sales-logs/#{sales_log.id}/property-information/check-answers")
end
end
end
end

375
spec/features/lettings_log_spec.rb

@ -729,380 +729,5 @@ RSpec.describe "Lettings Log Features" do
expect(duplicate_log.duplicate_set_id).to be_nil
end
end
context "when filling out address fields" do
let(:lettings_log) { create(:lettings_log, :setup_completed, assigned_to: user) }
before do
body = {
results: [
{
DPA: {
"POSTCODE": "AA1 1AA",
"POST_TOWN": "Bristol",
"ORGANISATION_NAME": "Some place",
},
},
],
}.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=111")
.to_return(status: 200, body:, headers: {})
body = { results: [{ DPA: { UPRN: "111" } }] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AA&key=OS_DATA_KEY&maxresults=10&minmatch=0.4")
.to_return(status: 200, body:, headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA11AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 1AA\",\"admin_district\":\"Westminster\",\"codes\":{\"admin_district\":\"E09000033\"}}}", headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA12AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 2AA\",\"admin_district\":\"Wigan\",\"codes\":{\"admin_district\":\"E08000010\"}}}", headers: {})
body = { results: [] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AB&key=OS_DATA_KEY&maxresults=10&minmatch=0.4")
.to_return(status: 200, body:, headers: {})
visit("/lettings-logs/#{lettings_log.id}/uprn")
end
context "and uprn is known and answered" do
before do
choose "Yes"
fill_in("lettings_log[uprn]", with: "111")
click_button("Save and continue")
end
context "and uprn is confirmed" do
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1) # yes
expect(lettings_log.uprn).to eq("111")
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Some Place")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("Bristol")
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
choose "Yes"
click_button("Save and continue")
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1) # yes
expect(lettings_log.uprn).to eq("111")
expect(lettings_log.uprn_confirmed).to eq(1) # yes
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Some Place")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("Bristol")
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
context "and changes to uprn not known" do
it "sets correct address fields" do
visit("/lettings-logs/#{lettings_log.id}/uprn")
choose "No"
click_button("Save and continue")
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
end
end
context "and uprn is not confirmed" do
before do
choose "No, I want to search for the address instead"
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
end
end
context "and uprn is not known" do
before do
choose "No"
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq(nil)
expect(lettings_log.postcode_full_input).to eq(nil)
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
context "and the address is not found" do
it "sets correct address fields" do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AB")
click_button("Search")
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AB")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
click_button("Confirm and continue")
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AB")
expect(lettings_log.address_search_value_check).to eq(0)
expect(lettings_log.la).to eq(nil)
end
end
context "and address is found, re-searched and not found" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
visit("/lettings-logs/#{lettings_log.id}/address-matcher")
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AB")
click_button("Search")
end
it "routes to the correct page" do
expect(page).to have_current_path("/lettings-logs/#{lettings_log.id}/no-address-found")
end
end
context "and the user selects 'address_not_listed'" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq("uprn_not_listed")
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Address line 1")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
context "and the user enters a new address manually" do
context "without changing a valid postcode" do
before do
fill_in("lettings_log[town_or_city]", with: "Town")
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq("uprn_not_listed")
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Address line 1")
expect(lettings_log.address_line2).to eq("")
expect(lettings_log.town_or_city).to eq("Town")
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
end
context "with changing the postcode" do
before do
fill_in("lettings_log[town_or_city]", with: "Town")
fill_in("lettings_log[postcode_full]", with: "AA12AA")
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq("uprn_not_listed")
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 2AA")
expect(lettings_log.address_line1).to eq("Address line 1")
expect(lettings_log.address_line2).to eq("")
expect(lettings_log.town_or_city).to eq("Town")
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E08000010")
end
end
end
end
context "and the user selects 'address_not_listed' when partial postcode is entered" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(0) # no
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq("uprn_not_listed")
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq("Address line 1")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
end
context "and the user selects 'address_not_listed' and then changes their mind and selects an address" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
visit("/lettings-logs/#{lettings_log.id}/uprn-selection")
choose("lettings-log-uprn-selection-111-field", allow_label_click: true)
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1)
expect(lettings_log.uprn).to eq("111")
expect(lettings_log.uprn_confirmed).to eq(1)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Some Place")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("Bristol")
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
end
context "and possible addresses found and selected" do
before do
fill_in("lettings_log[address_line1_input]", with: "Address line 1")
fill_in("lettings_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose("lettings-log-uprn-selection-111-field", allow_label_click: true)
click_button("Save and continue")
end
it "sets correct address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1)
expect(lettings_log.uprn).to eq("111")
expect(lettings_log.uprn_confirmed).to eq(1)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("AA1 1AA")
expect(lettings_log.address_line1).to eq("Some Place")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("Bristol")
expect(lettings_log.address_line1_input).to eq("Address line 1")
expect(lettings_log.postcode_full_input).to eq("AA1 1AA")
expect(lettings_log.address_search_value_check).to eq(nil)
expect(lettings_log.la).to eq("E09000033")
end
end
end
end
end
end

375
spec/features/sales_log_spec.rb

@ -334,381 +334,6 @@ RSpec.describe "Sales Log Features" do
expect(page).to have_current_path("/sales-logs/bulk-uploads")
end
end
context "when filling out address fields" do
let(:sales_log) { create(:sales_log, :shared_ownership_setup_complete, assigned_to: user) }
before do
body = {
results: [
{
DPA: {
"POSTCODE": "AA1 1AA",
"POST_TOWN": "Bristol",
"ORGANISATION_NAME": "Some place",
},
},
],
}.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=111")
.to_return(status: 200, body:, headers: {})
body = { results: [{ DPA: { UPRN: "111" } }] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AA&key=OS_DATA_KEY&maxresults=10&minmatch=0.4")
.to_return(status: 200, body:, headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA11AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 1AA\",\"admin_district\":\"Westminster\",\"codes\":{\"admin_district\":\"E09000033\"}}}", headers: {})
WebMock.stub_request(:get, "https://api.postcodes.io/postcodes/AA12AA")
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"postcode\":\"AA1 2AA\",\"admin_district\":\"Wigan\",\"codes\":{\"admin_district\":\"E08000010\"}}}", headers: {})
body = { results: [] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?query=Address+line+1%2C+AA1+1AB&key=OS_DATA_KEY&maxresults=10&minmatch=0.4")
.to_return(status: 200, body:, headers: {})
visit("/sales-logs/#{sales_log.id}/uprn")
end
context "and uprn is known and answered" do
before do
choose "Yes"
fill_in("sales_log[uprn]", with: "111")
click_button("Save and continue")
end
context "and uprn is confirmed" do
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(1) # yes
expect(sales_log.uprn).to eq("111")
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Some Place")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Bristol")
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
choose "Yes"
click_button("Save and continue")
sales_log.reload
expect(sales_log.uprn_known).to eq(1) # yes
expect(sales_log.uprn).to eq("111")
expect(sales_log.uprn_confirmed).to eq(1) # yes
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Some Place")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Bristol")
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
context "and changes to uprn not known" do
it "sets correct address fields" do
visit("/sales-logs/#{sales_log.id}/uprn")
choose "No"
click_button("Save and continue")
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
end
end
end
context "and uprn is not confirmed" do
before do
choose "No, I want to search for the address instead"
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
end
end
end
context "and uprn is not known" do
before do
choose "No"
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq(nil)
expect(sales_log.postcode_full_input).to eq(nil)
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
end
context "and the address is not found" do
it "sets correct address fields" do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AB")
click_button("Search")
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AB")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
click_button("Confirm and continue")
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AB")
expect(sales_log.address_search_value_check).to eq(0)
expect(sales_log.la).to eq(nil)
end
end
context "and address is found, re-searched and not found" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
visit("/sales-logs/#{sales_log.id}/address-matcher")
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AB")
click_button("Search")
end
it "routes to the correct page" do
expect(page).to have_current_path("/sales-logs/#{sales_log.id}/no-address-found")
end
end
context "and the user selects 'address_not_listed'" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq("uprn_not_listed")
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Address line 1")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
context "and the user enters a new address manually" do
context "without changing a valid postcode" do
before do
fill_in("sales_log[town_or_city]", with: "Town")
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq("uprn_not_listed")
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Address line 1")
expect(sales_log.address_line2).to eq("")
expect(sales_log.town_or_city).to eq("Town")
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
end
context "with changing the postcode" do
before do
fill_in("sales_log[town_or_city]", with: "Town")
fill_in("sales_log[postcode_full]", with: "AA12AA")
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq("uprn_not_listed")
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 2AA")
expect(sales_log.address_line1).to eq("Address line 1")
expect(sales_log.address_line2).to eq("")
expect(sales_log.town_or_city).to eq("Town")
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E08000010")
end
end
end
end
context "and the user selects 'address_not_listed' when partial postcode is given" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(0) # no
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq("uprn_not_listed")
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq("Address line 1")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq(nil)
end
end
context "and the user selects 'address_not_listed' and then changes their mind and selects an address" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose "The address is not listed, I want to enter the address manually"
click_button("Save and continue")
visit("/sales-logs/#{sales_log.id}/uprn-selection")
choose("sales-log-uprn-selection-111-field", allow_label_click: true)
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(1)
expect(sales_log.uprn).to eq("111")
expect(sales_log.uprn_confirmed).to eq(1)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Some Place")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Bristol")
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
end
context "and possible addresses found and selected" do
before do
fill_in("sales_log[address_line1_input]", with: "Address line 1")
fill_in("sales_log[postcode_full_input]", with: "AA1 1AA")
click_button("Search")
choose("sales-log-uprn-selection-111-field", allow_label_click: true)
click_button("Save and continue")
end
it "sets correct address fields" do
sales_log.reload
expect(sales_log.uprn_known).to eq(1)
expect(sales_log.uprn).to eq("111")
expect(sales_log.uprn_confirmed).to eq(1)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("AA1 1AA")
expect(sales_log.address_line1).to eq("Some Place")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Bristol")
expect(sales_log.address_line1_input).to eq("Address line 1")
expect(sales_log.postcode_full_input).to eq("AA1 1AA")
expect(sales_log.address_search_value_check).to eq(nil)
expect(sales_log.la).to eq("E09000033")
end
end
end
end
end
context "when a log becomes a duplicate" do

2
spec/fixtures/files/sales_logs_csv_export_codes_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_labels_24.csv vendored

File diff suppressed because one or more lines are too long

2
spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv vendored

File diff suppressed because one or more lines are too long

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

111
spec/lib/tasks/correct_reasonpref_values_spec.rb

@ -0,0 +1,111 @@
require "rails_helper"
require "rake"
RSpec.describe "correct_reasonpref_values" do
describe ":correct_reasonpref_values", type: :task do
subject(:task) { Rake::Task["correct_reasonpref_values"] }
let(:organisation) { create(:organisation, rent_periods: [2]) }
let(:user) { create(:user, organisation:) }
before do
Rake.application.rake_require("tasks/correct_reasonpref_values")
Rake::Task.define_task(:environment)
task.reenable
end
context "when the rake task is run" do
context "and any of the reasonable_preference_reason options are not 1, 0 or nil" do
let(:bulk_upload) { create(:bulk_upload, :lettings, year: 2024, rent_type_fix_status: BulkUpload.rent_type_fix_statuses[:not_applied]) }
it "sets the options to 0" do
log = build(:lettings_log, :completed, reasonpref: 1, rp_homeless: -2, rp_hardship: 2, rp_medwel: 3, rp_insan_unsat: 4, rp_dontknow: 1,
bulk_upload:, assigned_to: user)
log.save!(validate: false)
initial_updated_at = log.updated_at
task.invoke
log.reload
expect(log.updated_at).not_to eq(initial_updated_at)
expect(log.status).to eq("completed")
expect(log.rp_homeless).to be(0)
expect(log.rp_hardship).to be(0)
expect(log.rp_medwel).to be(0)
expect(log.rp_insan_unsat).to be(0)
expect(log.rp_dontknow).to be(1)
end
it "updates the reasonable preference reason values on a pending log" do
log = build(:lettings_log, :completed, status: "pending", reasonpref: 1, rp_homeless: -2, rp_hardship: 1, rp_medwel: 3, rp_insan_unsat: 4, rp_dontknow: 2, bulk_upload:, assigned_to: user)
log.save!(validate: false)
initial_updated_at = log.updated_at
expect(log.status).to eq("pending")
task.invoke
log.reload
expect(log.rp_homeless).to be(0)
expect(log.rp_hardship).to be(1)
expect(log.rp_medwel).to be(0)
expect(log.rp_insan_unsat).to be(0)
expect(log.rp_dontknow).to be(0)
expect(log.status).to eq("pending")
expect(log.updated_at).not_to eq(initial_updated_at)
end
it "does not update logs with valid values" do
log = build(:lettings_log, :completed, reasonpref: 1, rp_homeless: 0, rp_hardship: 1, rp_medwel: 0, rp_insan_unsat: 0, rp_dontknow: 0, bulk_upload:, assigned_to: user)
log.save!(validate: false)
initial_updated_at = log.updated_at
expect(log.status).to eq("completed")
task.invoke
log.reload
expect(log.status).to eq("completed")
expect(log.updated_at).to eq(initial_updated_at)
end
it "updates the reasonable preference reason values if some of the checkbox values are valid" do
log = build(:lettings_log, :completed, status: "pending", reasonpref: 1, rp_homeless: 0, rp_hardship: 2, rp_medwel: 1, rp_insan_unsat: 0, rp_dontknow: 0, bulk_upload:, assigned_to: user)
log.save!(validate: false)
initial_updated_at = log.updated_at
expect(log.status).to eq("pending")
task.invoke
log.reload
expect(log.rp_homeless).to be(0)
expect(log.rp_hardship).to be(0)
expect(log.rp_medwel).to be(1)
expect(log.rp_insan_unsat).to be(0)
expect(log.rp_dontknow).to be(0)
expect(log.status).to eq("pending")
expect(log.updated_at).not_to eq(initial_updated_at)
end
it "updates the reasonable preference reason values on a 2023 log" do
log = build(:lettings_log, :completed, startdate: Time.zone.local(2023, 6, 6), reasonpref: 1, rp_homeless: 0, rp_hardship: 2, rp_medwel: 1, rp_insan_unsat: 0, rp_dontknow: 0, bulk_upload:, assigned_to: user)
log.save!(validate: false)
initial_updated_at = log.updated_at
task.invoke
log.reload
expect(log.updated_at).to eq(initial_updated_at)
expect(log.rp_hardship).to eq(0)
end
it "does not update and logs error if a validation triggers" do
log = build(:lettings_log, :completed, postcode_full: "0", reasonpref: 1, rp_homeless: 0, rp_hardship: 2, rp_medwel: 1, rp_insan_unsat: 0, rp_dontknow: 0, bulk_upload:, assigned_to: user)
log.save!(validate: false)
initial_updated_at = log.updated_at
task.invoke
log.reload
expect(log.updated_at).to eq(initial_updated_at)
end
end
end
end
end

5
spec/models/bulk_upload_spec.rb

@ -24,7 +24,7 @@ RSpec.describe BulkUpload, type: :model do
let(:log) { build(:lettings_log, startdate: Time.zone.local(2025, 4, 2), 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 address_search_value_check multiple_partners_value_check partner_under_16_value_check reasonother_value_check]
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]
end
end
@ -32,7 +32,7 @@ RSpec.describe BulkUpload, type: :model do
let(:log) { build(:sales_log, :saledate_today, 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 student_not_child_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 address_search_value_check multiple_partners_value_check partner_under_16_value_check]
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 student_not_child_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]
end
end
end
@ -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]) }

9
spec/models/form/lettings/pages/address_fallback_spec.rb

@ -24,13 +24,6 @@ RSpec.describe Form::Lettings::Pages::AddressFallback, type: :model do
end
it "has correct depends_on" do
expect(page.depends_on).to eq([
{ "is_supported_housing?" => false, "uprn_known" => nil, "uprn_selection" => "uprn_not_listed" },
{ "is_supported_housing?" => false, "uprn_known" => 0, "uprn_selection" => "uprn_not_listed" },
{ "is_supported_housing?" => false, "uprn_confirmed" => 0, "uprn_selection" => "uprn_not_listed" },
{ "is_supported_housing?" => false, "uprn_known" => nil, "address_options_present?" => false },
{ "is_supported_housing?" => false, "uprn_known" => 0, "address_options_present?" => false },
{ "is_supported_housing?" => false, "uprn_confirmed" => 0, "address_options_present?" => false },
])
expect(page.depends_on).to eq([{ "manual_address_entry_selected" => true, "is_supported_housing?" => false }])
end
end

42
spec/models/form/lettings/pages/address_search_spec.rb

@ -0,0 +1,42 @@
require "rails_helper"
RSpec.describe Form::Lettings::Pages::AddressSearch, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) }
let(:start_date) { Time.utc(2024, 4, 1) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[uprn])
end
it "has the correct id" do
expect(page.id).to eq("address_search")
end
it "has the correct description" do
expect(page.description).to be_nil
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "is_supported_housing?" => false, "manual_address_entry_selected" => false }])
end
it "has the correct question number" do
expect(page.question_number).to eq(12)
end
context "with 2025/26 form" do
let(:start_date) { Time.utc(2025, 4, 1) }
it "has the correct question number" do
expect(page.question_number).to eq(16)
end
end
end

68
spec/models/form/lettings/questions/address_search_spec.rb

@ -0,0 +1,68 @@
require "rails_helper"
RSpec.describe Form::Lettings::Questions::AddressSearch, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date:))) }
let(:start_date) { Time.utc(2024, 4, 1) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("uprn")
end
it "has the correct type" do
expect(question.type).to eq("address_search")
end
it "has the correct question number" do
expect(question.question_number).to eq(12)
end
context "with 2025/26 form" do
let(:start_date) { Time.utc(2025, 4, 1) }
it "has the correct question number" do
expect(question.question_number).to eq(16)
end
end
describe "get_extra_check_answer_value" do
context "when address is not present" do
let(:log) { build(:lettings_log, manual_address_entry_selected: false) }
it "returns nil" do
expect(question.get_extra_check_answer_value(log)).to be_nil
end
end
context "when address search is present" do
let(:log) do
build(
:lettings_log,
:completed,
address_line1: "19, Charlton Gardens",
town_or_city: "Bristol",
postcode_full: "BS10 6LU",
la: "E06000023",
uprn_known: 1,
uprn: 107,
uprn_confirmed: 1,
)
end
context "when uprn known" do
it "returns formatted value" do
expect(question.get_extra_check_answer_value(log)).to eq(
"\n\n19, Charlton Gardens\nBristol\nBS10 6LU\nBristol, City of",
)
end
end
end
end
end

8
spec/models/form/lettings/questions/uprn_selection_spec.rb

@ -90,9 +90,10 @@ RSpec.describe Form::Lettings::Questions::UprnSelection, type: :model do
context "when the log has address line 1 input only" do
before do
allow(address_client_instance).to receive(:result).and_return(nil)
log.address_line1_input = "Address line 1"
log.postcode_full_input = nil
log.save!(valudate: false)
log.save!(validate: false)
end
it "has the correct input_playback" do
@ -102,9 +103,10 @@ RSpec.describe Form::Lettings::Questions::UprnSelection, type: :model do
context "when the log has postcode input only" do
before do
allow(address_client_instance).to receive(:result).and_return(nil)
log.address_line1_input = nil
log.postcode_full_input = "A1 1AA"
log.save!(valudate: false)
log.save!(validate: false)
end
it "has the correct input_playback" do
@ -116,7 +118,7 @@ RSpec.describe Form::Lettings::Questions::UprnSelection, type: :model do
before do
log.address_line1_input = "Address line 1"
log.postcode_full_input = "A1 1AA"
log.save!(valudate: false)
log.save!(validate: false)
end
it "has the correct input_playback" do

4
spec/models/form/lettings/questions/uprn_spec.rb

@ -52,12 +52,14 @@ RSpec.describe Form::Lettings::Questions::Uprn, type: :model do
la: "E09000003",
uprn_known:,
uprn:,
manual_address_entry_selected:,
)
end
context "when uprn known nil" do
let(:uprn_known) { nil }
let(:uprn) { nil }
let(:manual_address_entry_selected) { true }
it "returns formatted value" do
expect(question.get_extra_check_answer_value(log)).to be_nil
@ -67,6 +69,7 @@ RSpec.describe Form::Lettings::Questions::Uprn, type: :model do
context "when uprn known" do
let(:uprn_known) { 1 }
let(:uprn) { 1 }
let(:manual_address_entry_selected) { false }
it "returns formatted value" do
expect(question.get_extra_check_answer_value(log)).to eq(
@ -78,6 +81,7 @@ RSpec.describe Form::Lettings::Questions::Uprn, type: :model do
context "when uprn not known" do
let(:uprn_known) { 0 }
let(:uprn) { nil }
let(:manual_address_entry_selected) { true }
it "returns formatted value" do
expect(question.get_extra_check_answer_value(log)).to be_nil

12
spec/models/form/lettings/subsections/property_information_spec.rb

@ -64,11 +64,7 @@ RSpec.describe Form::Lettings::Subsections::PropertyInformation, type: :model do
it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq(
%w[
uprn
uprn_confirmation
address_matcher
no_address_found
uprn_selection
address_search
address
property_local_authority
local_authority_rent_value_check
@ -101,11 +97,7 @@ RSpec.describe Form::Lettings::Subsections::PropertyInformation, type: :model do
it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq(
%w[
uprn
uprn_confirmation
address_matcher
no_address_found
uprn_selection
address_search
address
property_local_authority
local_authority_rent_value_check

9
spec/models/form/sales/pages/address_fallback_spec.rb

@ -24,13 +24,6 @@ RSpec.describe Form::Sales::Pages::AddressFallback, type: :model do
end
it "has correct depends_on" do
expect(page.depends_on).to eq([
{ "uprn_known" => nil, "uprn_selection" => "uprn_not_listed" },
{ "uprn_known" => 0, "uprn_selection" => "uprn_not_listed" },
{ "uprn_confirmed" => 0, "uprn_selection" => "uprn_not_listed" },
{ "uprn_known" => nil, "address_options_present?" => false },
{ "uprn_known" => 0, "address_options_present?" => false },
{ "uprn_confirmed" => 0, "address_options_present?" => false },
])
expect(page.depends_on).to eq([{ "manual_address_entry_selected" => true }])
end
end

42
spec/models/form/sales/pages/address_search_spec.rb

@ -0,0 +1,42 @@
require "rails_helper"
RSpec.describe Form::Sales::Pages::AddressSearch, type: :model do
subject(:page) { described_class.new(page_id, page_definition, subsection) }
let(:page_id) { nil }
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection, form: instance_double(Form, start_date:)) }
let(:start_date) { Time.utc(2024, 4, 1) }
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end
it "has correct questions" do
expect(page.questions.map(&:id)).to eq(%w[uprn])
end
it "has the correct id" do
expect(page.id).to eq("address_search")
end
it "has the correct description" do
expect(page.description).to be_nil
end
it "has correct depends_on" do
expect(page.depends_on).to eq([{ "manual_address_entry_selected" => false }])
end
it "has the correct question number" do
expect(page.question_number).to eq(15)
end
context "with 2025/26 form" do
let(:start_date) { Time.utc(2025, 4, 1) }
it "has the correct question number" do
expect(page.question_number).to eq(13)
end
end
end

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

4
spec/models/form/sales/pages/uprn_confirmation_spec.rb

@ -7,6 +7,10 @@ RSpec.describe Form::Sales::Pages::UprnConfirmation, type: :model do
let(:page_definition) { nil }
let(:subsection) { instance_double(Form::Subsection) }
before do
allow(subsection).to receive(:form).and_return(instance_double(Form, start_year_2024_or_later?: false))
end
it "has correct subsection" do
expect(page.subsection).to eq(subsection)
end

68
spec/models/form/sales/questions/address_search_spec.rb

@ -0,0 +1,68 @@
require "rails_helper"
RSpec.describe Form::Sales::Questions::AddressSearch, type: :model do
subject(:question) { described_class.new(question_id, question_definition, page) }
let(:question_id) { nil }
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, subsection: instance_double(Form::Subsection, form: instance_double(Form, start_date:))) }
let(:start_date) { Time.utc(2024, 4, 1) }
it "has correct page" do
expect(question.page).to eq(page)
end
it "has the correct id" do
expect(question.id).to eq("uprn")
end
it "has the correct type" do
expect(question.type).to eq("address_search")
end
it "has the correct question number" do
expect(question.question_number).to eq(15)
end
context "with 2025/26 form" do
let(:start_date) { Time.utc(2025, 4, 1) }
it "has the correct question number" do
expect(question.question_number).to eq(13)
end
end
describe "get_extra_check_answer_value" do
context "when address is not present" do
let(:log) { build(:sales_log, manual_address_entry_selected: false) }
it "returns nil" do
expect(question.get_extra_check_answer_value(log)).to be_nil
end
end
context "when address search is present" do
let(:log) do
build(
:sales_log,
:completed,
address_line1: "19, Charlton Gardens",
town_or_city: "Bristol",
postcode_full: "BS10 6LU",
la: "E06000023",
uprn_known: 1,
uprn: 107,
uprn_confirmed: 1,
)
end
context "when uprn known" do
it "returns formatted value" do
expect(question.get_extra_check_answer_value(log)).to eq(
"\n\n19, Charlton Gardens\nBristol\nBS10 6LU\nBristol, City of",
)
end
end
end
end
end

8
spec/models/form/sales/questions/uprn_selection_spec.rb

@ -90,9 +90,10 @@ RSpec.describe Form::Sales::Questions::UprnSelection, type: :model do
context "when the log has address line 1 input only" do
before do
allow(address_client_instance).to receive(:result).and_return(nil)
log.address_line1_input = "Address line 1"
log.postcode_full_input = nil
log.save!(valudate: false)
log.save!(validate: false)
end
it "has the correct input_playback" do
@ -102,9 +103,10 @@ RSpec.describe Form::Sales::Questions::UprnSelection, type: :model do
context "when the log has postcode input only" do
before do
allow(address_client_instance).to receive(:result).and_return(nil)
log.address_line1_input = nil
log.postcode_full_input = "A1 1AA"
log.save!(valudate: false)
log.save!(validate: false)
end
it "has the correct input_playback" do
@ -116,7 +118,7 @@ RSpec.describe Form::Sales::Questions::UprnSelection, type: :model do
before do
log.address_line1_input = "Address line 1"
log.postcode_full_input = "A1 1AA"
log.save!(valudate: false)
log.save!(validate: false)
end
it "has the correct input_playback" do

12
spec/models/form/sales/subsections/property_information_spec.rb

@ -55,11 +55,7 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq(
%w[
uprn
uprn_confirmation
address_matcher
no_address_found
uprn_selection
address_search
address
property_local_authority
local_authority_buyer_1_income_max_value_check
@ -89,11 +85,7 @@ RSpec.describe Form::Sales::Subsections::PropertyInformation, type: :model do
it "has correct pages" do
expect(property_information.pages.map(&:id)).to eq(
%w[
uprn
uprn_confirmation
address_matcher
no_address_found
uprn_selection
address_search
address
property_local_authority
local_authority_buyer_1_income_max_value_check

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)

7
spec/models/sales_log_spec.rb

@ -566,6 +566,7 @@ RSpec.describe SalesLog, type: :model do
ppostcode_full: nil,
prevloc: nil,
saledate: Time.zone.local(2024, 5, 2),
manual_address_entry_selected: true,
})
end
@ -617,7 +618,7 @@ RSpec.describe SalesLog, type: :model do
end
let(:address_sales_log_25_26) do
create(:sales_log, :shared_ownership_setup_complete, postcode_full: "CA10 1AA", saledate: Time.zone.local(2025, 5, 2))
create(:sales_log, :shared_ownership_setup_complete, postcode_full: "CA10 1AA", saledate: Time.zone.local(2025, 5, 2), manual_address_entry_selected: true)
end
before do
@ -672,11 +673,11 @@ RSpec.describe SalesLog, type: :model do
context "when saving address with LAs that have changed E-codes" do
context "when address inferred from uprn - we still get LA from postcode" do
let(:address_sales_log_24_25) do
create(:sales_log, :shared_ownership_setup_complete, uprn_known: 1, uprn: 1, saledate: Time.zone.local(2024, 5, 2))
create(:sales_log, :shared_ownership_setup_complete, manual_address_entry_selected: false, uprn_known: 1, uprn: 1, saledate: Time.zone.local(2024, 5, 2))
end
let(:address_sales_log_25_26) do
create(:sales_log, :shared_ownership_setup_complete, uprn_known: 1, uprn: 1, saledate: Time.zone.local(2025, 5, 2))
create(:sales_log, :shared_ownership_setup_complete, manual_address_entry_selected: false, uprn_known: 1, uprn: 1, saledate: Time.zone.local(2025, 5, 2))
end
before do

4
spec/models/validations/sales/property_validations_spec.rb

@ -136,9 +136,9 @@ RSpec.describe Validations::Sales::PropertyValidations do
context "when within the limit and only numeric" do
let(:record) { build(:sales_log, uprn: "123456789012") }
it "does not add an error" do
it "does not add an invalid UPRN error" do
property_validator.validate_uprn(record)
expect(record.errors).not_to be_present
expect(record.errors.added?(:uprn, I18n.t("validations.sales.property_information.uprn.invalid"))).to be false
end
end
end

2
spec/request_helper.rb

@ -20,7 +20,7 @@ module RequestHelper
body = { results: [{ DPA: { UPRN: "10033558653" } }] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?key&maxresults=10&minmatch=0.4&query=Address%20line%201,%20SW1A%201AA")
.to_return(status: 200, body:, headers: {})
body = { results: [{ DPA: { "POSTCODE": "SW1A 1AA", "POST_TOWN": "London" } }] }.to_json
body = { results: [{ DPA: { "POSTCODE": "SW1A 1AA", "POST_TOWN": "London", "PO_BOX_NUMBER": "The Mall, City Of Westminster" } }] }.to_json
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key&uprn=1")
.to_return(status: 200, body:, headers: {})
WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key&uprn=10033558653")

148
spec/requests/address_search_controller_spec.rb

@ -0,0 +1,148 @@
require "rails_helper"
RSpec.describe AddressSearchController, type: :request do
let(:user) { create(:user) }
before do
sign_in user
end
describe "#manual input" do
context "when no address data is given and user chooses to enter address manually" do
let(:sales_log) { create(:sales_log, :shared_ownership_setup_complete, manual_address_entry_selected: false, assigned_to: user) }
it "correctly sets address fields" do
sales_log.reload
expect(sales_log.manual_address_entry_selected).to eq(false)
expect(sales_log.uprn_known).to eq(nil)
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.la).to eq(nil)
get "/address-search/manual-input/sales_log/#{sales_log.id}"
sales_log.reload
expect(sales_log.manual_address_entry_selected).to eq(true)
expect(sales_log.uprn_known).to eq(0)
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.la).to eq(nil)
end
end
context "when choosing to manually input an address for a log that has an address searched value" do
let(:lettings_log) { create(:lettings_log, :completed, manual_address_entry_selected: false, assigned_to: user) }
it "correctly sets address fields" do
lettings_log.reload
expect(lettings_log.uprn_known).to eq(1)
expect(lettings_log.uprn).to eq("10033558653")
expect(lettings_log.uprn_confirmed).to eq(1)
expect(lettings_log.uprn_selection).to eq("10033558653")
expect(lettings_log.postcode_known).to eq(1)
expect(lettings_log.postcode_full).to eq("SW1A 1AA")
expect(lettings_log.address_line1).to eq("The Mall, City Of Westminster")
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq("London")
expect(lettings_log.la).to eq("E09000033")
get "/address-search/manual-input/lettings_log/#{lettings_log.id}"
lettings_log.reload
expect(lettings_log.manual_address_entry_selected).to eq(true)
expect(lettings_log.uprn_known).to eq(0)
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
end
end
describe "#search input" do
context "when no address is entered manually and choosing to search instead" do
let(:lettings_log) { create(:lettings_log, :setup_completed, manual_address_entry_selected: true, assigned_to: user) }
it "correctly sets address fields" do
lettings_log.reload
expect(lettings_log.manual_address_entry_selected).to eq(true)
expect(lettings_log.uprn_known).to eq(0)
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.la).to eq(nil)
get "/address-search/search-input/lettings_log/#{lettings_log.id}"
lettings_log.reload
expect(lettings_log.manual_address_entry_selected).to eq(false)
expect(lettings_log.uprn_known).to eq(nil)
expect(lettings_log.uprn).to eq(nil)
expect(lettings_log.uprn_confirmed).to eq(nil)
expect(lettings_log.uprn_selection).to eq(nil)
expect(lettings_log.postcode_known).to eq(nil)
expect(lettings_log.postcode_full).to eq(nil)
expect(lettings_log.address_line1).to eq(nil)
expect(lettings_log.address_line2).to eq(nil)
expect(lettings_log.town_or_city).to eq(nil)
expect(lettings_log.la).to eq(nil)
end
end
context "when choosing to search for an address for a log that has an address searched value" do
let(:sales_log) { create(:sales_log, :completed, manual_address_entry_selected: true, town_or_city: "Test Town", assigned_to: user) }
it "correctly sets address fields" do
sales_log.reload
expect(sales_log.manual_address_entry_selected).to eq(true)
expect(sales_log.uprn_known).to eq(0)
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(0)
expect(sales_log.postcode_full).to eq("SW1A 1AA")
expect(sales_log.address_line1).to eq("Address line 1")
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq("Test Town")
expect(sales_log.la).to eq("E09000033")
get "/address-search/search-input/sales_log/#{sales_log.id}"
sales_log.reload
expect(sales_log.manual_address_entry_selected).to eq(false)
expect(sales_log.uprn_known).to eq(nil)
expect(sales_log.uprn).to eq(nil)
expect(sales_log.uprn_confirmed).to eq(nil)
expect(sales_log.uprn_selection).to eq(nil)
expect(sales_log.pcodenk).to eq(nil)
expect(sales_log.postcode_full).to eq(nil)
expect(sales_log.address_line1).to eq(nil)
expect(sales_log.address_line2).to eq(nil)
expect(sales_log.town_or_city).to eq(nil)
expect(sales_log.la).to eq(nil)
end
end
end
end

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

Loading…
Cancel
Save