diff --git a/.rake_tasks~ b/.rake_tasks~ new file mode 100644 index 000000000..e69de29bb diff --git a/app/components/create_log_actions_component.html.erb b/app/components/create_log_actions_component.html.erb index 4b74c8901..2c3b75aea 100644 --- a/app/components/create_log_actions_component.html.erb +++ b/app/components/create_log_actions_component.html.erb @@ -1,5 +1,5 @@
"> -<% 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 %>
diff --git a/app/components/create_log_actions_component.rb b/app/components/create_log_actions_component.rb index 0abbfd385..ae240bc91 100644 --- a/app/components/create_log_actions_component.rb +++ b/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 diff --git a/app/controllers/address_search_controller.rb b/app/controllers/address_search_controller.rb new file mode 100644 index 000000000..616d5b702 --- /dev/null +++ b/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 diff --git a/app/controllers/test_data_controller.rb b/app/controllers/test_data_controller.rb index 2b049f176..8617ff3b5 100644 --- a/app/controllers/test_data_controller.rb +++ b/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 - return render_not_found unless FeatureToggle.create_test_logs_enabled? + %w[2024 2025].each do |year| + define_method("create_#{year}_test_lettings_bulk_upload") do + return render_not_found unless FeatureToggle.create_test_logs_enabled? - file = Tempfile.new("test_lettings_log.csv") - log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA") - log_to_csv = BulkUpload::LettingsLogToCsv.new(log:, line_ending: "\n", overrides: { organisation_id: "ORG#{log.owning_organisation_id}", managing_organisation_id: "ORG#{log.owning_organisation_id}" }) - file.write(log_to_csv.default_field_numbers_row) - file.write(log_to_csv.to_csv_row) - file.rewind - send_file file.path, type: "text/csv", - filename: "test_lettings_log.csv", - disposition: "attachment", - after_send: lambda { - file.close - file.unlink - } + file = Tempfile.new("#{year}_test_lettings_log.csv") + log = FactoryBot.create(:lettings_log, :completed, assigned_to: current_user, ppostcode_full: "SW1A 1AA", startdate: Time.zone.local(year.to_i, rand(4..12), rand(1..28))) + log_to_csv = BulkUpload::LettingsLogToCsv.new(log:, line_ending: "\n", overrides: { organisation_id: "ORG#{log.owning_organisation_id}", managing_organisation_id: "ORG#{log.owning_organisation_id}" }) + file.write(log_to_csv.default_field_numbers_row) + file.write(log_to_csv.to_csv_row) + file.rewind + send_file file.path, type: "text/csv", + filename: "#{year}_test_lettings_log.csv", + disposition: "attachment", + after_send: lambda { + file.close + file.unlink + } + end end + def create_2025_test_sales_bulk_upload; end + def create_test_sales_log return render_not_found unless FeatureToggle.create_test_logs_enabled? - 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 - return render_not_found unless FeatureToggle.create_test_logs_enabled? - - file = Tempfile.new("test_sales_log.csv") + [2024, 2025].each do |year| + define_method("create_#{year}_test_sales_bulk_upload") do + return render_not_found unless FeatureToggle.create_test_logs_enabled? - log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, value: 180_000, deposit: 150_000) - log_to_csv = BulkUpload::SalesLogToCsv.new(log:, line_ending: "\n", overrides: { organisation_id: "ORG#{log.owning_organisation_id}", managing_organisation_id: "ORG#{log.owning_organisation_id}" }) - file.write(log_to_csv.default_field_numbers_row) - file.write(log_to_csv.to_csv_row) - file.rewind - send_file file.path, type: "text/csv", - filename: "test_sales_log.csv", - disposition: "attachment", - after_send: lambda { - file.close - file.unlink - } + file = Tempfile.new("#{year}_test_sales_log.csv") + log = FactoryBot.create(:sales_log, :completed, assigned_to: current_user, value: 180_000, deposit: 150_000, county: "Somerset", saledate: Time.zone.local(year.to_i, rand(4..12), rand(1..28))) + log_to_csv = BulkUpload::SalesLogToCsv.new(log:, line_ending: "\n", overrides: { organisation_id: "ORG#{log.owning_organisation_id}", managing_organisation_id: "ORG#{log.owning_organisation_id}" }) + file.write(log_to_csv.default_field_numbers_row) + file.write(log_to_csv.to_csv_row) + file.rewind + send_file file.path, + type: "text/csv", + filename: "#{year}_test_sales_log.csv", + disposition: "attachment", + after_send: lambda { + file.close + file.unlink + } + end end end diff --git a/app/frontend/controllers/address_search_controller.js b/app/frontend/controllers/address_search_controller.js new file mode 100644 index 000000000..de54090ec --- /dev/null +++ b/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 + } + } + }) + } +} diff --git a/app/frontend/controllers/index.js b/app/frontend/controllers/index.js index 944e32e2d..fa7944045 100644 --- a/app/frontend/controllers/index.js +++ b/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) diff --git a/app/frontend/styles/_task-list.scss b/app/frontend/styles/_task-list.scss index f8e2128dd..a739bc245 100644 --- a/app/frontend/styles/_task-list.scss +++ b/app/frontend/styles/_task-list.scss @@ -52,3 +52,7 @@ margin-bottom: 0; } } + +.app-red-text { + color: govuk-colour("red"); +} diff --git a/app/helpers/bulk_upload/lettings_log_to_csv.rb b/app/helpers/bulk_upload/lettings_log_to_csv.rb index cdf38db17..b81fec3d7 100644 --- a/app/helpers/bulk_upload/lettings_log_to_csv.rb +++ b/app/helpers/bulk_upload/lettings_log_to_csv.rb @@ -17,12 +17,8 @@ class BulkUpload::LettingsLogToCsv def to_csv_row(seed: nil) year = log.collection_start_year case year - when 2022 - to_2022_csv_row(seed:) - when 2023 - to_2023_csv_row(seed:) - when 2024 - to_2024_csv_row(seed:) + when 2022, 2023, 2024, 2025 + to_year_csv_row(year, seed:) else raise NotImplementedError "No mapping function implemented for year #{year}" end @@ -30,82 +26,32 @@ class BulkUpload::LettingsLogToCsv def to_row year = log.collection_start_year - case year - when 2022 - to_2022_row - when 2023 - to_2023_row - when 2024 - to_2024_row - else - raise NotImplementedError "No mapping function implemented for year #{year}" - end + send("to_#{year}_row") + rescue NoMethodError + raise NotImplementedError "No mapping function implemented for year #{year}" end def default_field_numbers_row(seed: nil) year = log.collection_start_year - case year - when 2022 - default_2022_field_numbers_row(seed:) - when 2023 - default_2023_field_numbers_row(seed:) - when 2024 - default_2024_field_numbers_row(seed:) - else - raise NotImplementedError "No mapping function implemented for year #{year}" - end + default_field_numbers_row_for_year(year, seed:) + rescue NoMethodError + raise NotImplementedError "No mapping function implemented for year #{year}" end def default_field_numbers year = log.collection_start_year - case year - when 2022 - default_2022_field_numbers - when 2023 - default_2023_field_numbers - when 2024 - default_2024_field_numbers - else - raise NotImplementedError "No mapping function implemented for year #{year}" - end + send("default_#{year}_field_numbers") + rescue NoMethodError + raise NotImplementedError "No mapping function implemented for year #{year}" end - def to_2022_csv_row(seed: nil) + def to_year_csv_row(year, seed: nil) + unshuffled_row = send("to_#{year}_row") if seed - row = to_2022_row.shuffle(random: Random.new(seed)) + row = unshuffled_row.shuffle(random: Random.new(seed)) (row_prefix + row).flatten.join(",") + line_ending else - (row_prefix + to_2022_row).flatten.join(",") + line_ending - end - end - - def default_2022_field_numbers - (1..134).to_a - end - - def default_2022_field_numbers_row(seed: nil) - if seed - ["Field number"] + default_2022_field_numbers.shuffle(random: Random.new(seed)) - else - ["Field number"] + default_2022_field_numbers - end.flatten.join(",") + line_ending - end - - def to_2023_csv_row(seed: nil) - if seed - row = to_2023_row.shuffle(random: Random.new(seed)) - (row_prefix + row).flatten.join(",") + line_ending - else - (row_prefix + to_2023_row).flatten.join(",") + line_ending - end - end - - def to_2024_csv_row(seed: nil) - if seed - row = to_2024_row.shuffle(random: Random.new(seed)) - (row_prefix + row).flatten.join(",") + line_ending - else - (row_prefix + to_2024_row).flatten.join(",") + line_ending + (row_prefix + unshuffled_row).flatten.join(",") + line_ending end end @@ -121,20 +67,16 @@ class BulkUpload::LettingsLogToCsv ] end - def default_2023_field_numbers_row(seed: nil) + def default_field_numbers_row_for_year(year, seed: nil) if seed - ["Field number"] + default_2023_field_numbers.shuffle(random: Random.new(seed)) + ["Field number"] + send("default_#{year}_field_numbers").shuffle(random: Random.new(seed)) else - ["Field number"] + default_2023_field_numbers + ["Field number"] + send("default_#{year}_field_numbers") end.flatten.join(",") + line_ending end - def default_2024_field_numbers_row(seed: nil) - if seed - ["Field number"] + default_2024_field_numbers.shuffle(random: Random.new(seed)) - else - ["Field number"] + default_2024_field_numbers - end.flatten.join(",") + line_ending + def default_2022_field_numbers + (1..134).to_a end def default_2023_field_numbers @@ -145,6 +87,156 @@ class BulkUpload::LettingsLogToCsv (1..130).to_a end + def default_2025_field_numbers + (1..129).to_a + end + + def to_2025_row + [ + overrides[:organisation_id] || log.owning_organisation&.old_visible_id, # 1 + overrides[:managing_organisation_id] || log.managing_organisation&.old_visible_id, + log.assigned_to&.email, + log.needstype, + log.scheme&.id ? "S#{log.scheme&.id}" : "", + log.location&.id, + renewal, + log.startdate&.day, + log.startdate&.month, + log.startdate&.strftime("%y"), # 10 + + rent_type, + log.irproduct_other, + log.tenancycode, + log.propcode, + log.declaration, + log.rsnvac, + log.unitletas, + log.uprn, + log.address_line1&.tr(",", " "), + log.address_line2&.tr(",", " "), # 20 + + log.town_or_city&.tr(",", " "), + log.county&.tr(",", " "), + ((log.postcode_full || "").split(" ") || [""]).first, + ((log.postcode_full || "").split(" ") || [""]).last, + log.la, + log.unittype_gn, + log.builtype, + log.wchair, + log.beds, + log.voiddate&.day, # 30 + + log.voiddate&.month, + log.voiddate&.strftime("%y"), + log.mrcdate&.day, + log.mrcdate&.month, + log.mrcdate&.strftime("%y"), + log.sheltered, + log.joint, + log.startertenancy, + log.tenancy, + log.tenancyother, # 40 + + log.tenancylength, + log.age1 || overrides[:age1], + log.sex1, + log.ethnic, + log.nationality_all_group, + log.ecstat1, + relat_number(log.relat2), + log.age2 || overrides[:age2], + log.sex2, + log.ecstat2, # 50 + + relat_number(log.relat3), + log.age3 || overrides[:age3], + log.sex3, + log.ecstat3, + relat_number(log.relat4), + log.age4 || overrides[:age4], + log.sex4, + log.ecstat4, + relat_number(log.relat5), + log.age5 || overrides[:age5], # 60 + + log.sex5, + log.ecstat5, + relat_number(log.relat6), + log.age6 || overrides[:age6], + log.sex6, + log.ecstat6, + relat_number(log.relat7), + log.age7 || overrides[:age7], + log.sex7, + log.ecstat7, # 70 + + relat_number(log.relat8), + log.age8 || overrides[:age8], + log.sex8, + log.ecstat8, + log.armedforces, + log.leftreg, + log.reservist, + log.preg_occ, + log.housingneeds_a, + log.housingneeds_b, # 80 + + log.housingneeds_c, + log.housingneeds_f, + log.housingneeds_g, + log.housingneeds_h, + overrides[:illness] || log.illness, + log.illness_type_1, + log.illness_type_2, + log.illness_type_3, + log.illness_type_4, + log.illness_type_5, # 90 + + log.illness_type_6, + log.illness_type_7, + log.illness_type_8, + log.illness_type_9, + log.illness_type_10, + log.layear, + log.waityear, + log.reason, + log.reasonother, + log.prevten, # 100 + + homeless, + previous_postcode_known, + ((log.ppostcode_full || "").split(" ") || [""]).first, + ((log.ppostcode_full || "").split(" ") || [""]).last, + log.prevloc, + log.reasonpref, + log.rp_homeless, + log.rp_insan_unsat, + log.rp_medwel, + log.rp_hardship, # 110 + + log.rp_dontknow, + cbl, + chr, + cap, + accessible_register, + log.referral, + net_income_known, + log.incfreq, + log.earnings, + log.hb, # 120 + + log.benefits, + log.household_charge, + log.period, + log.brent, + log.scharge, + log.pscharge, + log.supcharg, + log.hbrentshortfall, + log.tshortfall, # 129 + ] + end + def to_2024_row [ overrides[:organisation_id] || log.owning_organisation&.old_visible_id, # 1 @@ -551,4 +643,15 @@ private log.hhregres end end + + def relat_number(value) + case value + when "P" + 1 + when "R" + 3 + when "C", "X" + 2 + end + end end diff --git a/app/helpers/bulk_upload/sales_log_to_csv.rb b/app/helpers/bulk_upload/sales_log_to_csv.rb index bc1435186..5fad19e74 100644 --- a/app/helpers/bulk_upload/sales_log_to_csv.rb +++ b/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)) diff --git a/app/helpers/collection_deadline_helper.rb b/app/helpers/collection_deadline_helper.rb index 3c4f7e502..de9c12075 100644 --- a/app/helpers/collection_deadline_helper.rb +++ b/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 diff --git a/app/helpers/tasklist_helper.rb b/app/helpers/tasklist_helper.rb index ff52f4094..631f4d315 100644 --- a/app/helpers/tasklist_helper.rb +++ b/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 + "

Upcoming #{log_quarter.quarter} deadline: #{log_quarter.cutoff_date.strftime('%-d %B %Y')}.

".html_safe + else + "

Overdue: #{log_quarter.quarter} deadline #{log_quarter.cutoff_date.strftime('%-d %B %Y')}.

".html_safe + end + end + private def breadcrumb_organisation(log) diff --git a/app/models/bulk_upload.rb b/app/models/bulk_upload.rb index 6616285b0..dd09b365b 100644 --- a/app/models/bulk_upload.rb +++ b/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 diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index 12d96c2bc..d8b366972 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/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 diff --git a/app/models/derived_variables/sales_log_variables.rb b/app/models/derived_variables/sales_log_variables.rb index ef4997283..04658c222 100644 --- a/app/models/derived_variables/sales_log_variables.rb +++ b/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 diff --git a/app/models/form/lettings/pages/address_fallback.rb b/app/models/form/lettings/pages/address_fallback.rb index fd580a3ed..f7503e3af 100644 --- a/app/models/form/lettings/pages/address_fallback.rb +++ b/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 diff --git a/app/models/form/lettings/pages/address_search.rb b/app/models/form/lettings/pages/address_search.rb new file mode 100644 index 000000000..866018d45 --- /dev/null +++ b/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 diff --git a/app/models/form/lettings/questions/address_search.rb b/app/models/form/lettings/questions/address_search.rb new file mode 100644 index 000000000..ed7edf894 --- /dev/null +++ b/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 diff --git a/app/models/form/lettings/questions/postcode_for_full_address.rb b/app/models/form/lettings/questions/postcode_for_full_address.rb index a4c775a55..114cf0db5 100644 --- a/app/models/form/lettings/questions/postcode_for_full_address.rb +++ b/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 diff --git a/app/models/form/lettings/subsections/property_information.rb b/app/models/form/lettings/subsections/property_information.rb index 4d3e022c0..a24cd1092 100644 --- a/app/models/form/lettings/subsections/property_information.rb +++ b/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 diff --git a/app/models/form/sales/pages/address_fallback.rb b/app/models/form/sales/pages/address_fallback.rb index b6818ae0c..9465a494f 100644 --- a/app/models/form/sales/pages/address_fallback.rb +++ b/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 diff --git a/app/models/form/sales/pages/address_search.rb b/app/models/form/sales/pages/address_search.rb new file mode 100644 index 000000000..f0d58af58 --- /dev/null +++ b/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 diff --git a/app/models/form/sales/pages/living_before_purchase.rb b/app/models/form/sales/pages/living_before_purchase.rb index b8797537b..3e2df425a 100644 --- a/app/models/form/sales/pages/living_before_purchase.rb +++ b/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? diff --git a/app/models/form/sales/pages/no_address_found.rb b/app/models/form/sales/pages/no_address_found.rb index ae2663896..e0ff5bcb2 100644 --- a/app/models/form/sales/pages/no_address_found.rb +++ b/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 diff --git a/app/models/form/sales/questions/address_search.rb b/app/models/form/sales/questions/address_search.rb new file mode 100644 index 000000000..d6acbaba1 --- /dev/null +++ b/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 diff --git a/app/models/form/sales/questions/living_before_purchase_years.rb b/app/models/form/sales/questions/living_before_purchase_years.rb index d2df3209b..010aa6edc 100644 --- a/app/models/form/sales/questions/living_before_purchase_years.rb +++ b/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) diff --git a/app/models/form/sales/questions/postcode_for_full_address.rb b/app/models/form/sales/questions/postcode_for_full_address.rb index 95656a108..e99ec108b 100644 --- a/app/models/form/sales/questions/postcode_for_full_address.rb +++ b/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 diff --git a/app/models/form/sales/subsections/property_information.rb b/app/models/form/sales/subsections/property_information.rb index 28c0ad004..e33666208 100644 --- a/app/models/form/sales/subsections/property_information.rb +++ b/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), diff --git a/app/models/forms/bulk_upload_form/prepare_your_file.rb b/app/models/forms/bulk_upload_form/prepare_your_file.rb index 911daa4fe..7f6cfd759 100644 --- a/app/models/forms/bulk_upload_form/prepare_your_file.rb +++ b/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 diff --git a/app/models/log.rb b/app/models/log.rb index d55289997..1454aef65 100644 --- a/app/models/log.rb +++ b/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 - return @address_options if @address_options && @last_searched_address_string == address_string + 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 = [] diff --git a/app/services/address_client.rb b/app/services/address_client.rb index 81c8da7ed..20cf603fe 100644 --- a/app/services/address_client.rb +++ b/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 diff --git a/app/services/bulk_upload/lettings/log_creator.rb b/app/services/bulk_upload/lettings/log_creator.rb index 0df59b310..ab5405381 100644 --- a/app/services/bulk_upload/lettings/log_creator.rb +++ b/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 diff --git a/app/services/bulk_upload/lettings/validator.rb b/app/services/bulk_upload/lettings/validator.rb index 291bf45e7..8cb3a1bd9 100644 --- a/app/services/bulk_upload/lettings/validator.rb +++ b/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 diff --git a/app/services/bulk_upload/lettings/year2024/row_parser.rb b/app/services/bulk_upload/lettings/year2024/row_parser.rb index 3f5227c44..461469200 100644 --- a/app/services/bulk_upload/lettings/year2024/row_parser.rb +++ b/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 diff --git a/app/services/bulk_upload/lettings/year2025/csv_parser.rb b/app/services/bulk_upload/lettings/year2025/csv_parser.rb new file mode 100644 index 000000000..ec6c33b6d --- /dev/null +++ b/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 diff --git a/app/services/bulk_upload/lettings/year2025/row_parser.rb b/app/services/bulk_upload/lettings/year2025/row_parser.rb new file mode 100644 index 000000000..9332fbabd --- /dev/null +++ b/app/services/bulk_upload/lettings/year2025/row_parser.rb @@ -0,0 +1,1667 @@ +class BulkUpload::Lettings::Year2025::RowParser + include ActiveModel::Model + include ActiveModel::Attributes + include InterruptionScreenHelper + include FormattingHelper + + QUESTIONS = { + field_1: "Which organisation owns this property?", + field_2: "Which organisation manages this letting?", + field_3: "What is the CORE username of the account this letting log should be assigned to?", + field_4: "What is the needs type?", + field_5: "What scheme does this letting belong to?", + field_6: "Which location is this letting for?", + field_7: "Is this letting a renewal?", + field_8: "What is the tenancy start date?", + field_9: "What is the tenancy start date?", + field_10: "What is the tenancy start date?", + field_11: "What is the rent type?", + field_12: "Which 'Other' type of Intermediate Rent is this letting?", + field_13: "What is the tenant code?", + field_14: "What is the property reference?", + field_15: "Has tenant seen the MHCLG privacy notice?", + field_16: "What is the reason for the property being vacant?", + field_17: "What type was the property most recently let as?", + field_18: "If known, provide this property’s UPRN", + field_19: "Address line 1", + field_20: "Address line 2", + field_21: "Town or city", + field_22: "County", + field_23: "Part 1 of the property's postcode", + field_24: "Part 2 of the property's postcode", + field_25: "What is the property's local authority?", + field_26: "What type of unit is the property?", + field_27: "Which type of building is the property?", + field_28: "Is the property built or adapted to wheelchair-user standards?", + field_29: "How many bedrooms does the property have?", + field_30: "What is the void date?", + field_31: "What is the void date?", + field_32: "What is the void date?", + field_33: "What date were any major repairs completed on?", + field_34: "What date were any major repairs completed on?", + field_35: "What date were any major repairs completed on?", + field_36: "Is this letting sheltered accommodation?", + field_37: "Is this a joint tenancy?", + field_38: "Is this a starter tenancy?", + field_39: "What is the type of tenancy?", + field_40: "If 'Other', what is the type of tenancy?", + field_41: "What is the length of the fixed-term tenancy to the nearest year?", + field_42: "What is the lead tenant's age?", + field_43: "Which of these best describes the lead tenant's gender identity?", + field_44: "Which of these best describes the lead tenant's ethnic background?", + field_45: "What is the lead tenant's nationality?", + field_46: "Which of these best describes the lead tenant's working situation?", + field_47: "Is person 2 the partner of the lead tenant?", + field_48: "What is person 2's age?", + field_49: "Which of these best describes person 2's gender identity?", + field_50: "Which of these best describes person 2's working situation?", + field_51: "Is person 3 the partner of the lead tenant?", + field_52: "What is person 3's age?", + field_53: "Which of these best describes person 3's gender identity?", + field_54: "Which of these best describes person 3's working situation?", + field_55: "Is person 4 the partner of the lead tenant?", + field_56: "What is person 4's age?", + field_57: "Which of these best describes person 4's gender identity?", + field_58: "Which of these best describes person 4's working situation?", + field_59: "Is person 5 the partner of the lead tenant?", + field_60: "What is person 5's age?", + field_61: "Which of these best describes person 5's gender identity?", + field_62: "Which of these best describes person 5's working situation?", + field_63: "Is person 6 the partner of the lead tenant?", + field_64: "What is person 6's age?", + field_65: "Which of these best describes person 6's gender identity?", + field_66: "Which of these best describes person 6's working situation?", + field_67: "Is person 7 the partner of the lead tenant?", + field_68: "What is person 7's age?", + field_69: "Which of these best describes person 7's gender identity?", + field_70: "Which of these best describes person 7's working situation?", + field_71: "Is person 8 the partner of the lead tenant?", + field_72: "What is person 8's age?", + field_73: "Which of these best describes person 8's gender identity?", + field_74: "Which of these best describes person 8's working situation?", + field_75: "Does anybody in the household have links to the UK armed forces?", + field_76: "Is this person still serving in the UK armed forces?", + field_77: "Was this person seriously injured or ill as a result of serving in the UK armed forces?", + field_78: "Is anybody in the household pregnant?", + field_79: "Does anybody in the household have any disabled access needs?", + field_80: "Does anybody in the household have any disabled access needs?", + field_81: "Does anybody in the household have any disabled access needs?", + field_82: "Does anybody in the household have any disabled access needs?", + field_83: "Does anybody in the household have any disabled access needs?", + field_84: "Does anybody in the household have any disabled access needs?", + field_85: "Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more?", + field_86: "Does this person's condition affect their dexterity?", + field_87: "Does this person's condition affect their learning or understanding or concentrating?", + field_88: "Does this person's condition affect their hearing?", + field_89: "Does this person's condition affect their memory?", + field_90: "Does this person's condition affect their mental health?", + field_91: "Does this person's condition affect their mobility?", + field_92: "Does this person's condition affect them socially or behaviourally?", + field_93: "Does this person's condition affect their stamina or breathing or fatigue?", + field_94: "Does this person's condition affect their vision?", + field_95: "Does this person's condition affect them in another way?", + field_96: "How long has the household continuously lived in the local authority area of the new letting?", + field_97: "How long has the household been on the local authority waiting list for the new letting?", + field_98: "What is the tenant’s main reason for the household leaving their last settled home?", + field_99: "If 'Other', what was the main reason for leaving their last settled home?", + field_100: "Where was the household immediately before this letting?", + field_101: "Did the household experience homelessness immediately before this letting?", + field_102: "Do you know the postcode of the household's last settled home?", + field_103: "What is the postcode of the household's last settled home?", + field_104: "What is the postcode of the household's last settled home?", + field_105: "What is the local authority of the household's last settled home?", + field_106: "Was the household given 'reasonable preference' by the local authority?", + field_107: "Reasonable preference reason They were homeless or about to lose their home (within 56 days)", + field_108: "Reasonable preference reason They were living in insanitary, overcrowded or unsatisfactory housing", + field_109: "Reasonable preference reason They needed to move on medical and welfare reasons (including disability)", + field_110: "Reasonable preference reason They needed to move to avoid hardship to themselves or others", + field_111: "Reasonable preference reason Don't know", + field_112: "Was the letting made under the Choice-Based Lettings (CBL)?", + field_113: "Was the letting made under the Common Allocation Policy (CAP)?", + field_114: "Was the letting made under the Common Housing Register (CHR)?", + field_115: "Was the letting made under the Accessible Register?", + field_116: "What was the source of referral for this letting?", + field_117: "Do you know the household's combined total income after tax?", + field_118: "How often does the household receive income?", + field_119: "How much income does the household have in total?", + field_120: "Is the tenant likely to be receiving any of these housing-related benefits?", + field_121: "How much of the household's income is from Universal Credit, state pensions or benefits?", + field_122: "Does the household pay rent or other charges for the accommodation?", + field_123: "How often does the household pay rent and other charges?", + field_124: "What is the basic rent?", + field_125: "What is the service charge?", + field_126: "What is the personal service charge?", + field_127: "What is the support charge?", + field_128: "After the household has received any housing-related benefits, will they still need to pay for rent and charges?", + field_129: "What do you expect the outstanding amount to be?", + }.freeze + + RENT_TYPE_BU_MAPPING = { + 1 => 0, + 2 => 1, + 3 => 2, + 4 => 3, + 5 => 4, + 6 => 5, + 7 => 6, + }.freeze + + ERROR_BASE_KEY = "validations.lettings.2025.bulk_upload".freeze + + attribute :bulk_upload + attribute :block_log_creation, :boolean, default: -> { false } + + attribute :field_blank + + attribute :field_1, :string + attribute :field_2, :string + attribute :field_3, :string + attribute :field_4, :integer + attribute :field_7, :integer + attribute :field_8, :integer + attribute :field_9, :integer + attribute :field_10, :integer + attribute :field_11, :integer + attribute :field_12, :string + attribute :field_13, :string + attribute :field_14, :string + attribute :field_5, :string + attribute :field_6, :string + attribute :field_18, :string + attribute :field_19, :string + attribute :field_20, :string + attribute :field_21, :string + attribute :field_22, :string + attribute :field_23, :string + attribute :field_24, :string + attribute :field_25, :string + attribute :field_17, :integer + attribute :field_16, :integer + attribute :field_26, :integer + attribute :field_27, :integer + attribute :field_28, :integer + attribute :field_29, :integer + attribute :field_30, :integer + attribute :field_31, :integer + attribute :field_32, :integer + attribute :field_33, :integer + attribute :field_34, :integer + attribute :field_35, :integer + attribute :field_37, :integer + attribute :field_38, :integer + attribute :field_39, :integer + attribute :field_40, :string + attribute :field_41, :integer + attribute :field_36, :integer + attribute :field_15, :integer + attribute :field_42, :string + attribute :field_43, :string + attribute :field_44, :integer + attribute :field_45, :integer + attribute :field_46, :integer + attribute :field_47, :integer + attribute :field_48, :string + attribute :field_49, :string + attribute :field_50, :integer + attribute :field_51, :integer + attribute :field_52, :string + attribute :field_53, :string + attribute :field_54, :integer + attribute :field_55, :integer + attribute :field_56, :string + attribute :field_57, :string + attribute :field_58, :integer + attribute :field_59, :integer + attribute :field_60, :string + attribute :field_61, :string + attribute :field_62, :integer + attribute :field_63, :integer + attribute :field_64, :string + attribute :field_65, :string + attribute :field_66, :integer + attribute :field_67, :integer + attribute :field_68, :string + attribute :field_69, :string + attribute :field_70, :integer + attribute :field_71, :integer + attribute :field_72, :string + attribute :field_73, :string + attribute :field_74, :integer + attribute :field_75, :integer + attribute :field_76, :integer + attribute :field_77, :integer + attribute :field_78, :integer + attribute :field_79, :integer + attribute :field_80, :integer + attribute :field_81, :integer + attribute :field_82, :integer + attribute :field_83, :integer + attribute :field_84, :integer + attribute :field_85, :integer + attribute :field_86, :integer + attribute :field_87, :integer + attribute :field_88, :integer + attribute :field_89, :integer + attribute :field_90, :integer + attribute :field_91, :integer + attribute :field_92, :integer + attribute :field_93, :integer + attribute :field_94, :integer + attribute :field_95, :integer + attribute :field_96, :integer + attribute :field_97, :integer + attribute :field_98, :integer + attribute :field_99, :string + attribute :field_100, :integer + attribute :field_101, :integer + attribute :field_102, :integer + attribute :field_103, :string + attribute :field_104, :string + attribute :field_105, :string + attribute :field_106, :integer + attribute :field_107, :integer + attribute :field_108, :integer + attribute :field_109, :integer + attribute :field_110, :integer + attribute :field_111, :integer + attribute :field_112, :integer + attribute :field_113, :integer + attribute :field_114, :integer + attribute :field_115, :integer + attribute :field_116, :integer + attribute :field_117, :integer + attribute :field_118, :integer + attribute :field_119, :decimal + attribute :field_120, :integer + attribute :field_121, :integer + attribute :field_122, :integer + attribute :field_123, :integer + attribute :field_124, :decimal + attribute :field_125, :decimal + attribute :field_126, :decimal + attribute :field_127, :decimal + attribute :field_128, :integer + attribute :field_129, :decimal + + validate :validate_valid_radio_option, on: :before_log + + validates :field_11, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "rent type."), + category: :setup, + }, + inclusion: { + in: (1..7).to_a, + message: I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: "rent type."), + unless: -> { field_11.blank? }, + category: :setup, + }, + on: :after_log + + validates :field_7, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "property renewal."), + category: :setup, + }, + on: :after_log + + validates :field_8, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "tenancy start date (day)."), + category: :setup, + }, + on: :after_log + + validates :field_9, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "tenancy start date (month)."), + category: :setup, + }, + on: :after_log + + validates :field_10, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "tenancy start date (year)."), + category: :setup, + }, + format: { + with: /\A(\d{2}|\d{4})\z/, + message: I18n.t("#{ERROR_BASE_KEY}.startdate.year_not_two_or_four_digits"), + category: :setup, + unless: -> { field_10.blank? }, + }, + on: :after_log + + validates :field_5, + presence: { + if: proc { supported_housing? }, + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "scheme code."), + category: :setup, + }, + on: :after_log + + validates :field_6, + presence: { + if: proc { supported_housing? }, + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "location code."), + category: :setup, + }, + on: :after_log + + validates :field_112, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "was the letting made under the Choice-Based Lettings (CBL)?"), + category: :not_answered, + }, + inclusion: { + in: [1, 2], + message: I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: "was the letting made under the Choice-Based Lettings (CBL)?"), + if: -> { field_112.present? }, + }, + on: :after_log + + validates :field_113, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "was the letting made under the Common Allocation Policy (CAP)?"), + category: :not_answered, + }, + inclusion: { + in: [1, 2], + message: I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: "was the letting made under the Common Allocation Policy (CAP)?"), + if: -> { field_113.present? }, + }, + on: :after_log + + validates :field_114, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "was the letting made under the Common Housing Register (CHR)?"), + category: :not_answered, + }, + inclusion: { + in: [1, 2], + message: I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: "was the letting made under the Common Housing Register (CHR)?"), + if: -> { field_114.present? }, + }, + on: :after_log + + validates :field_115, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "was the letting made under the Accessible Register?"), + category: :not_answered, + }, + inclusion: { + in: [1, 2], + message: I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: "was the letting made under the Accessible Register?"), + if: -> { field_115.present? }, + }, + on: :after_log + + validates :field_42, format: { with: /\A\d{1,3}\z|\AR\z/, message: I18n.t("#{ERROR_BASE_KEY}.age.invalid", person_num: 1) }, on: :after_log + validates :field_48, format: { with: /\A\d{1,3}\z|\AR\z/, message: I18n.t("#{ERROR_BASE_KEY}.age.invalid", person_num: 2) }, on: :after_log, if: proc { details_known?(2).zero? } + validates :field_52, format: { with: /\A\d{1,3}\z|\AR\z/, message: I18n.t("#{ERROR_BASE_KEY}.age.invalid", person_num: 3) }, on: :after_log, if: proc { details_known?(3).zero? } + validates :field_56, format: { with: /\A\d{1,3}\z|\AR\z/, message: I18n.t("#{ERROR_BASE_KEY}.age.invalid", person_num: 4) }, on: :after_log, if: proc { details_known?(4).zero? } + validates :field_60, format: { with: /\A\d{1,3}\z|\AR\z/, message: I18n.t("#{ERROR_BASE_KEY}.age.invalid", person_num: 5) }, on: :after_log, if: proc { details_known?(5).zero? } + validates :field_64, format: { with: /\A\d{1,3}\z|\AR\z/, message: I18n.t("#{ERROR_BASE_KEY}.age.invalid", person_num: 6) }, on: :after_log, if: proc { details_known?(6).zero? } + validates :field_68, format: { with: /\A\d{1,3}\z|\AR\z/, message: I18n.t("#{ERROR_BASE_KEY}.age.invalid", person_num: 7) }, on: :after_log, if: proc { details_known?(7).zero? } + validates :field_72, format: { with: /\A\d{1,3}\z|\AR\z/, message: I18n.t("#{ERROR_BASE_KEY}.age.invalid", person_num: 8) }, on: :after_log, if: proc { details_known?(8).zero? } + + validate :validate_needs_type_present, on: :after_log + validate :validate_data_types, on: :after_log + validate :validate_relevant_collection_window, on: :after_log + validate :validate_la_with_local_housing_referral, on: :after_log + validate :validate_cannot_be_la_referral_if_general_needs_and_la, on: :after_log + validate :validate_leaving_reason_for_renewal, on: :after_log + validate :validate_only_one_housing_needs_type, on: :after_log + validate :validate_no_disabled_needs_conjunction, on: :after_log + validate :validate_dont_know_disabled_needs_conjunction, on: :after_log + validate :validate_no_and_dont_know_disabled_needs_conjunction, on: :after_log + validate :validate_no_housing_needs_questions_answered, on: :after_log + validate :validate_reasonable_preference_homeless, on: :after_log + validate :validate_condition_effects, on: :after_log + validate :validate_if_log_already_exists, on: :after_log, if: -> { FeatureToggle.bulk_upload_duplicate_log_check_enabled? } + + validate :validate_owning_org_data_given, on: :after_log + validate :validate_owning_org_exists, on: :after_log + validate :validate_owning_org_owns_stock, on: :after_log + validate :validate_owning_org_permitted, on: :after_log + + validate :validate_managing_org_data_given, on: :after_log + validate :validate_managing_org_exists, on: :after_log + validate :validate_managing_org_related, on: :after_log + + validate :validate_related_scheme_exists, on: :after_log + validate :validate_related_location_exists, on: :after_log + + validate :validate_assigned_to_exists, on: :after_log + validate :validate_assigned_to_related, on: :after_log + validate :validate_assigned_to_when_support, on: :after_log + validate :validate_all_charges_given, on: :after_log + + validate :validate_address_option_found, on: :after_log, unless: -> { supported_housing? } + validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log, unless: -> { supported_housing? } + validate :validate_address_fields, on: :after_log, unless: -> { supported_housing? } + + 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 + + def self.question_for_field(field) + QUESTIONS[field] + end + + def valid? + return @valid if @valid + + errors.clear + + return @valid = true if blank_row? + + super(:before_log) + @before_errors = errors.dup + + log.valid? + + super(:after_log) + errors.merge!(@before_errors) + + log.errors.each do |error| + fields = field_mapping_for_errors[error.attribute] || [] + + fields.each do |field| + next if errors.include?(field) + next if error.type == :skip_bu_error + + question = log.form.get_question(error.attribute, log) + + if question.present? && setup_question?(question) + errors.add(field, error.message, category: :setup) + else + errors.add(field, error.message) + end + end + end + + @valid = errors.blank? + end + + def blank_row? + attribute_set + .to_hash + .reject { |k, _| %w[bulk_upload block_log_creation field_blank].include?(k) } + .values + .reject(&:blank?) + .compact + .empty? + end + + def log + @log ||= LettingsLog.new(attributes_for_log) + end + + def block_log_creation! + self.block_log_creation = true + end + + def block_log_creation? + block_log_creation + end + + def tenant_code + field_13 + end + + def property_ref + field_14 + end + + def log_already_exists? + return false if blank_row? + + @log_already_exists ||= LettingsLog + .where(status: %w[not_started in_progress completed]) + .exists?(duplicate_check_fields.index_with { |field| log.public_send(field) }) + end + + def spreadsheet_duplicate_hash + hash = attributes.slice( + "field_1", # owning org + "field_8", # startdate + "field_9", # startdate + "field_10", # startdate + "field_13", # tenancycode + !general_needs? ? :field_6.to_s : nil, # location + !supported_housing? ? "field_23" : nil, # postcode + !supported_housing? ? "field_24" : nil, # postcode + "field_42", # age1 + "field_43", # sex1 + "field_46", # ecstat1 + ) + if [field_124, field_125, field_126, field_127].all?(&:present?) + hash.merge({ "tcharge" => [field_124, field_125, field_126, field_127].sum }) + else + hash + end + end + + def add_duplicate_found_in_spreadsheet_errors + spreadsheet_duplicate_hash.each_key do |field| + if field == "tcharge" + %w[field_124 field_125 field_126 field_127].each do |sub_field| + errors.add(sub_field, I18n.t("#{ERROR_BASE_KEY}.spreadsheet_dupe"), category: :setup) + end + else + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.spreadsheet_dupe"), category: :setup) + end + end + end + +private + + def validate_valid_radio_option + log.attributes.each do |question_id, _v| + question = log.form.get_question(question_id, log) + + next unless question&.type == "radio" + next if log[question_id].blank? || question.answer_options.key?(log[question_id].to_s) || !question.page.routed_to?(log, nil) + + fields = field_mapping_for_errors[question_id.to_sym] || [] + + fields.each do |field| + if setup_question?(question) + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field])), category: :setup) + else + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field]))) + end + end + end + end + + def validate_assigned_to_exists + return if field_3.blank? + + unless assigned_to + errors.add(:field_3, I18n.t("#{ERROR_BASE_KEY}.assigned_to.not_found")) + end + end + + def validate_assigned_to_when_support + if field_3.blank? && bulk_upload.user.support? + errors.add(:field_3, category: :setup, message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "what is the CORE username of the account this letting log should be assigned to?")) + end + end + + def validate_assigned_to_related + return unless assigned_to + return if assigned_to.organisation == owning_organisation || assigned_to.organisation == managing_organisation + return if assigned_to.organisation == owning_organisation&.absorbing_organisation || assigned_to.organisation == managing_organisation&.absorbing_organisation + + block_log_creation! + errors.add(:field_3, I18n.t("#{ERROR_BASE_KEY}.assigned_to.organisation_not_related")) + end + + def assigned_to + @assigned_to ||= User.where("lower(email) = ?", field_3&.downcase).first + end + + def validate_uprn_exists_if_any_key_address_fields_are_blank + if field_18.blank? && !key_address_fields_provided? + %i[field_19 field_21 field_23 field_24].each do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_answered")) if send(field).blank? + end + errors.add(:field_18, I18n.t("#{ERROR_BASE_KEY}.address.not_answered", question: "UPRN.")) + end + end + + def validate_address_option_found + if log.uprn.nil? && field_18.blank? && key_address_fields_provided? + error_message = if log.address_options_present? && log.address_options.size > 1 + I18n.t("#{ERROR_BASE_KEY}.address.not_determined.multiple") + elsif log.address_options_present? + I18n.t("#{ERROR_BASE_KEY}.address.not_determined.one") + else + I18n.t("#{ERROR_BASE_KEY}.address.not_found") + end + %i[field_19 field_20 field_21 field_22 field_23 field_24].each do |field| + errors.add(field, error_message) if errors[field].blank? + end + end + end + + def key_address_fields_provided? + field_19.present? && field_21.present? && postcode_full.present? + end + + def validate_address_fields + if field_18.blank? || log.errors.attribute_names.include?(:uprn) + if field_19.blank? && errors[:field_19].blank? + errors.add(:field_19, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1.")) + end + + if field_21.blank? && errors[:field_21].blank? + errors.add(:field_21, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city.")) + end + + if field_23.blank? && errors[:field_23].blank? + errors.add(:field_23, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode.")) + end + + if field_24.blank? && errors[:field_24].blank? + errors.add(:field_24, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode.")) + end + end + end + + def validate_incomplete_soft_validations + routed_to_soft_validation_questions = log.form.questions.filter { |q| q.type == "interruption_screen" && q.page.routed_to?(log, nil) }.compact + routed_to_soft_validation_questions.each do |question| + next if question.completed?(log) + + question.page.interruption_screen_question_ids.each do |interruption_screen_question_id| + next if log.form.questions.none? { |q| q.id == interruption_screen_question_id && q.page.routed_to?(log, nil) } + + field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field| + if errors.none? { |e| field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) } + error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ") + errors.add(field, message: error_message, category: :soft_validation) + end + end + end + end + end + + def validate_nationality + if field_45.present? && !valid_nationality_options.include?(field_45.to_s) + errors.add(:field_45, I18n.t("#{ERROR_BASE_KEY}.nationality.invalid")) + 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", + "age1", + "sex1", + "ecstat1", + "owning_organisation", + "tcharge", + !supported_housing? ? "postcode_full" : nil, + !general_needs? ? "location" : nil, + "tenancycode", + log.chcharge.present? ? "chcharge" : nil, + ].compact + end + + def validate_needs_type_present + if field_4.blank? + errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "needs type."), category: :setup) + end + end + + def validate_no_and_dont_know_disabled_needs_conjunction + if field_83 == 1 && field_84 == 1 + errors.add(:field_83, I18n.t("#{ERROR_BASE_KEY}.housingneeds.no_and_dont_know_disabled_needs_conjunction")) + errors.add(:field_84, I18n.t("#{ERROR_BASE_KEY}.housingneeds.no_and_dont_know_disabled_needs_conjunction")) + end + end + + def validate_dont_know_disabled_needs_conjunction + if field_84 == 1 && [field_79, field_80, field_81, field_82].count(1).positive? + %i[field_84 field_79 field_80 field_81 field_82].each do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.housingneeds.dont_know_disabled_needs_conjunction")) if send(field) == 1 + end + end + end + + def validate_no_disabled_needs_conjunction + if field_83 == 1 && [field_79, field_80, field_81, field_82].count(1).positive? + %i[field_83 field_79 field_80 field_81 field_82].each do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.housingneeds.no_disabled_needs_conjunction")) if send(field) == 1 + end + end + end + + def validate_only_one_housing_needs_type + if [field_79, field_80, field_81].count(1) > 1 + %i[field_79 field_80 field_81].each do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.housingneeds_type.only_one_option_permitted")) if send(field) == 1 + end + end + end + + def validate_no_housing_needs_questions_answered + if [field_79, field_80, field_81, field_82, field_83, field_84].all?(&:blank?) + errors.add(:field_83, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "anybody with disabled access needs.")) + errors.add(:field_82, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "other access needs.")) + %i[field_79 field_80 field_81].each do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "disabled access needs type.")) + end + end + end + + def validate_reasonable_preference_homeless + reason_fields = %i[field_107 field_108 field_109 field_110 field_111] + if field_106 == 1 && reason_fields.all? { |field| attributes[field.to_s].blank? } + reason_fields.each do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "reason for reasonable preference.")) + end + end + end + + def validate_condition_effects + illness_option_fields = %i[field_94 field_88 field_91 field_86 field_87 field_89 field_90 field_93 field_92 field_95] + if household_no_illness? + illness_option_fields.each do |field| + if attributes[field.to_s] == 1 + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.condition_effects.no_choices")) + end + end + elsif illness_option_fields.all? { |field| attributes[field.to_s].blank? } + illness_option_fields.each do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "how is person affected by condition or illness.")) + end + end + end + + def household_no_illness? + field_85 != 1 + end + + def validate_leaving_reason_for_renewal + if field_7 == 1 && ![50, 51, 52, 53].include?(field_98) + errors.add(:field_98, I18n.t("#{ERROR_BASE_KEY}.reason.renewal_reason_needed")) + end + end + + def general_needs? + field_4 == 1 + end + + def supported_housing? + field_4 == 2 + end + + def validate_cannot_be_la_referral_if_general_needs_and_la + if field_116 == 4 && general_needs? && owning_organisation && owning_organisation.la? + errors.add :field_116, I18n.t("#{ERROR_BASE_KEY}.referral.general_needs_prp_referred_by_la") + end + end + + def validate_la_with_local_housing_referral + if field_116 == 3 && owning_organisation && owning_organisation.la? + errors.add(:field_116, I18n.t("#{ERROR_BASE_KEY}.referral.nominated_by_local_ha_but_la")) + end + end + + def validate_relevant_collection_window + return if startdate.blank? || bulk_upload.form.blank? + + unless bulk_upload.form.valid_start_date_for_form?(startdate) + errors.add(:field_8, I18n.t("#{ERROR_BASE_KEY}.startdate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + errors.add(:field_9, I18n.t("#{ERROR_BASE_KEY}.startdate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + errors.add(:field_10, I18n.t("#{ERROR_BASE_KEY}.startdate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + end + end + + def validate_data_types + unless attribute_set["field_11"].value_before_type_cast&.match?(/^\d+\.?0*$/) + errors.add(:field_11, I18n.t("#{ERROR_BASE_KEY}.invalid_number", question: "rent type.")) + end + end + + def validate_nulls + field_mapping_for_errors.each do |error_key, fields| + question_id = error_key.to_s + question = questions.find { |q| q.id == question_id } + + next unless question + next if log.optional_fields.include?(question.id) + next if question.completed?(log) + + if setup_question?(question) + fields.each do |field| + if field.present? && errors.none? { |e| fields.include?(e.attribute) } && @before_errors.none? { |e| fields.include?(e.attribute) } + errors.add(field, question.unanswered_error_message, category: :setup) + end + end + else + fields.each do |field| + if errors.none? { |e| fields.include?(e.attribute) } && @before_errors.none? { |e| fields.include?(e.attribute) } + errors.add(field, question.unanswered_error_message) + end + end + end + end + end + + def validate_related_location_exists + if scheme && field_6.present? && location.nil? && :field_6.present? + block_log_creation! + errors.add(:field_6, I18n.t("#{ERROR_BASE_KEY}.location.must_relate_to_org"), category: :setup) + end + end + + def validate_related_scheme_exists + if field_5.present? && :field_5.present? && owning_organisation.present? && managing_organisation.present? && scheme.nil? + block_log_creation! + errors.add(:field_5, I18n.t("#{ERROR_BASE_KEY}.scheme.must_relate_to_org"), category: :setup) + end + end + + def validate_managing_org_related + if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation) + block_log_creation! + + if errors[:field_2].blank? + errors.add(:field_2, I18n.t("#{ERROR_BASE_KEY}.managing_organisation.no_relationship"), category: :setup) + end + end + end + + def validate_managing_org_exists + if managing_organisation.nil? + block_log_creation! + + if field_2.present? && errors[:field_2].blank? + errors.add(:field_2, I18n.t("#{ERROR_BASE_KEY}.managing_organisation.not_found"), category: :setup) + end + end + end + + def validate_managing_org_data_given + if field_2.blank? + block_log_creation! + errors.add(:field_2, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "managing organisation."), category: :setup) + end + end + + def validate_owning_org_owns_stock + if owning_organisation && !owning_organisation.holds_own_stock? + block_log_creation! + + if errors[:field_1].blank? + errors.add(:field_1, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_stock_owner"), category: :setup) + end + end + end + + def validate_owning_org_exists + if owning_organisation.nil? + block_log_creation! + + if field_1.present? && errors[:field_1].blank? + errors.add(:field_1, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_found"), category: :setup) + end + end + end + + def validate_owning_org_data_given + if field_1.blank? + block_log_creation! + errors.add(:field_1, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "owning organisation."), category: :setup) + end + end + + def validate_owning_org_permitted + return unless owning_organisation + return if bulk_upload_organisation.affiliated_stock_owners.include?(owning_organisation) + + block_log_creation! + + return if errors[:field_1].present? + + if bulk_upload.user.support? + errors.add(:field_1, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_permitted.support", org_name: bulk_upload_organisation.name), category: :setup) + else + errors.add(:field_1, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_permitted.not_support"), category: :setup) + end + end + + def validate_all_charges_given + return if supported_housing? && field_124 == 1 + + blank_charge_fields, other_charge_fields = { + field_124: "basic rent", + field_125: "service charge", + field_126: "personal service charge", + field_127: "support charge", + }.partition { |field, _| public_send(field).blank? }.map(&:to_h) + + blank_charge_fields.each do |field, charge| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.charges.missing_charges", sentence_fragment: charge)) + end + + other_charge_fields.each do |field, _charge| + blank_charge_fields.each do |_blank_field, blank_charge| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.charges.missing_charges", sentence_fragment: blank_charge)) + end + end + end + + def all_charges_given? + field_124.present? && field_125.present? && field_126.present? && field_127.present? + end + + def setup_question?(question) + log.form.setup_sections[0].subsections[0].questions.include?(question) + end + + def validate_if_log_already_exists + if log_already_exists? + error_message = I18n.t("#{ERROR_BASE_KEY}.duplicate") + + errors.add(:field_1, error_message) # owning_organisation + errors.add(:field_8, error_message) # startdate + errors.add(:field_9, error_message) # startdate + errors.add(:field_10, error_message) # startdate + errors.add(:field_13, error_message) # tenancycode + errors.add(:field_6, error_message) if !general_needs? && :field_6.present? # location + errors.add(:field_5, error_message) if !general_needs? && :field_6.blank? # add to Scheme field as unclear whether log uses New or Old CORE ids + errors.add(:field_23, error_message) unless supported_housing? # postcode_full + errors.add(:field_24, error_message) unless supported_housing? # postcode_full + errors.add(:field_25, error_message) unless supported_housing? # la + errors.add(:field_42, error_message) # age1 + errors.add(:field_43, error_message) # sex1 + errors.add(:field_46, error_message) # ecstat1 + errors.add(:field_122, error_message) unless general_needs? # household_charge + errors.add(:field_124, error_message) # brent + errors.add(:field_125, error_message) # scharge + errors.add(:field_126, error_message) # pscharge + errors.add(:field_127, error_message) # chcharge + end + end + + def field_mapping_for_errors + { + lettype: [:field_11], + tenancycode: [:field_13], + postcode_known: %i[field_25 field_23 field_24], + postcode_full: %i[field_25 field_23 field_24], + la: %i[field_25], + owning_organisation: [:field_1], + managing_organisation: [:field_2], + owning_organisation_id: [:field_1], + managing_organisation_id: [:field_2], + renewal: [:field_7], + scheme_id: (:field_5.present? ? [:field_5] : nil), + scheme: (:field_5.present? ? [:field_5] : nil), + location_id: (:field_6.present? ? [:field_6] : nil), + location: (:field_6.present? ? [:field_6] : nil), + assigned_to: [:field_3], + needstype: [:field_4], + rent_type: %i[field_11], + startdate: %i[field_8 field_9 field_10], + unittype_gn: %i[field_26], + builtype: %i[field_27], + wchair: %i[field_28], + beds: %i[field_29], + joint: %i[field_37], + startertenancy: %i[field_38], + tenancy: %i[field_39], + tenancyother: %i[field_40], + tenancylength: %i[field_41], + declaration: %i[field_15], + + age1_known: %i[field_42], + age1: %i[field_42], + age2_known: %i[field_48], + age2: %i[field_48], + age3_known: %i[field_52], + age3: %i[field_52], + age4_known: %i[field_56], + age4: %i[field_56], + age5_known: %i[field_60], + age5: %i[field_60], + age6_known: %i[field_64], + age6: %i[field_64], + age7_known: %i[field_68], + age7: %i[field_68], + age8_known: %i[field_72], + age8: %i[field_72], + + sex1: %i[field_43], + sex2: %i[field_49], + sex3: %i[field_53], + sex4: %i[field_57], + sex5: %i[field_61], + sex6: %i[field_65], + sex7: %i[field_69], + sex8: %i[field_73], + + ethnic_group: %i[field_44], + ethnic: %i[field_44], + nationality_all: %i[field_45], + nationality_all_group: %i[field_45], + + relat2: %i[field_47], + relat3: %i[field_51], + relat4: %i[field_55], + relat5: %i[field_59], + relat6: %i[field_63], + relat7: %i[field_67], + relat8: %i[field_71], + + ecstat1: %i[field_46], + ecstat2: %i[field_50], + ecstat3: %i[field_54], + ecstat4: %i[field_58], + ecstat5: %i[field_62], + ecstat6: %i[field_66], + ecstat7: %i[field_70], + ecstat8: %i[field_74], + + armedforces: %i[field_75], + leftreg: %i[field_76], + reservist: %i[field_77], + preg_occ: %i[field_78], + housingneeds: %i[field_78], + + illness: %i[field_85], + + layear: %i[field_96], + waityear: %i[field_97], + reason: %i[field_98], + reasonother: %i[field_99], + prevten: %i[field_100], + homeless: %i[field_101], + + prevloc: %i[field_105], + previous_la_known: %i[field_105], + ppcodenk: %i[field_102], + ppostcode_full: %i[field_103 field_104], + + reasonpref: %i[field_106], + rp_homeless: %i[field_107], + rp_insan_unsat: %i[field_108], + rp_medwel: %i[field_109], + rp_hardship: %i[field_110], + rp_dontknow: %i[field_111], + + cbl: %i[field_112], + cap: %i[field_113], + chr: %i[field_114], + accessible_register: %i[field_115], + letting_allocation: %i[field_112 field_113 field_114 field_115], + + referral: %i[field_116], + + net_income_known: %i[field_117], + incfreq: %i[field_118], + earnings: %i[field_119], + hb: %i[field_120], + benefits: %i[field_121], + + period: %i[field_123], + brent: %i[field_124], + scharge: %i[field_125], + pscharge: %i[field_126], + supcharg: %i[field_127], + tcharge: %i[field_124 field_125 field_126 field_127], + household_charge: %i[field_122], + hbrentshortfall: %i[field_128], + tshortfall: %i[field_129], + + unitletas: %i[field_17], + rsnvac: %i[field_16], + sheltered: %i[field_36], + + illness_type_1: %i[field_94], + illness_type_2: %i[field_88], + illness_type_3: %i[field_91], + illness_type_4: %i[field_86], + illness_type_5: %i[field_87], + illness_type_6: %i[field_89], + illness_type_7: %i[field_90], + illness_type_8: %i[field_93], + illness_type_9: %i[field_92], + illness_type_10: %i[field_95], + + irproduct_other: %i[field_12], + + propcode: %i[field_14], + + majorrepairs: %i[field_33 field_34 field_35], + mrcdate: %i[field_33 field_34 field_35], + + voiddate: %i[field_30 field_31 field_32], + + uprn: [:field_18], + address_line1: [:field_19], + address_line2: [:field_20], + town_or_city: [:field_21], + county: [:field_22], + uprn_selection: [:field_19], + }.compact + end + + def attribute_set + @attribute_set ||= instance_variable_get(:@attributes) + end + + def questions + @questions ||= log.form.subsections.flat_map { |ss| ss.applicable_questions(log) } + end + + def attributes_for_log + attributes = {} + + attributes["lettype"] = nil # should get this from rent_type + attributes["tenancycode"] = field_13 + attributes["owning_organisation"] = owning_organisation + attributes["managing_organisation"] = managing_organisation + attributes["renewal"] = renewal + attributes["scheme"] = scheme + attributes["location"] = location + attributes["assigned_to"] = assigned_to || (bulk_upload.user.support? ? nil : bulk_upload.user) + attributes["created_by"] = bulk_upload.user + attributes["needstype"] = field_4 + attributes["rent_type"] = RENT_TYPE_BU_MAPPING[field_11] + attributes["startdate"] = startdate + attributes["unittype_gn"] = field_26 + attributes["builtype"] = field_27 + attributes["wchair"] = field_28 + attributes["beds"] = field_26 == 2 ? 1 : field_29 + attributes["joint"] = field_37 + attributes["startertenancy"] = field_38 + attributes["tenancy"] = field_39 + attributes["tenancyother"] = field_40 + attributes["tenancylength"] = field_41 + attributes["declaration"] = field_15 + + attributes["age1_known"] = age1_known? + attributes["age1"] = field_42 if attributes["age1_known"]&.zero? && field_42&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age2_known"] = age2_known? + attributes["age2"] = field_48 if attributes["age2_known"]&.zero? && field_48&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age3_known"] = age3_known? + attributes["age3"] = field_52 if attributes["age3_known"]&.zero? && field_52&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age4_known"] = age4_known? + attributes["age4"] = field_56 if attributes["age4_known"]&.zero? && field_56&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age5_known"] = age5_known? + attributes["age5"] = field_60 if attributes["age5_known"]&.zero? && field_60&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age6_known"] = age6_known? + attributes["age6"] = field_64 if attributes["age6_known"]&.zero? && field_64&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age7_known"] = age7_known? + attributes["age7"] = field_68 if attributes["age7_known"]&.zero? && field_68&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age8_known"] = age8_known? + attributes["age8"] = field_72 if attributes["age8_known"]&.zero? && field_72&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["sex1"] = field_43 + attributes["sex2"] = field_49 + attributes["sex3"] = field_53 + attributes["sex4"] = field_57 + attributes["sex5"] = field_61 + attributes["sex6"] = field_65 + attributes["sex7"] = field_69 + attributes["sex8"] = field_73 + + attributes["ethnic_group"] = ethnic_group_from_ethnic + attributes["ethnic"] = field_44 + attributes["nationality_all"] = field_45 if field_45.present? && valid_nationality_options.include?(field_45.to_s) + attributes["nationality_all_group"] = nationality_group(attributes["nationality_all"]) + + attributes["relat2"] = relationship_from_input_value(field_47) + attributes["relat3"] = relationship_from_input_value(field_51) + attributes["relat4"] = relationship_from_input_value(field_55) + attributes["relat5"] = relationship_from_input_value(field_59) + attributes["relat6"] = relationship_from_input_value(field_63) + attributes["relat7"] = relationship_from_input_value(field_67) + attributes["relat8"] = relationship_from_input_value(field_71) + + attributes["ecstat1"] = field_46 + attributes["ecstat2"] = field_50 + attributes["ecstat3"] = field_54 + attributes["ecstat4"] = field_58 + attributes["ecstat5"] = field_62 + attributes["ecstat6"] = field_66 + attributes["ecstat7"] = field_70 + attributes["ecstat8"] = field_74 + + attributes["details_known_2"] = details_known?(2) + attributes["details_known_3"] = details_known?(3) + attributes["details_known_4"] = details_known?(4) + attributes["details_known_5"] = details_known?(5) + attributes["details_known_6"] = details_known?(6) + attributes["details_known_7"] = details_known?(7) + attributes["details_known_8"] = details_known?(8) + + attributes["armedforces"] = field_75 + attributes["leftreg"] = leftreg + attributes["reservist"] = field_77 + + attributes["preg_occ"] = field_78 + + attributes["housingneeds"] = housingneeds + attributes["housingneeds_type"] = housingneeds_type + attributes["housingneeds_other"] = housingneeds_other + + attributes["illness"] = field_85 + + attributes["layear"] = field_96 + attributes["waityear"] = field_97 + attributes["reason"] = field_98 + attributes["reasonother"] = field_99 if reason_is_other? + attributes["prevten"] = field_100 + attributes["homeless"] = field_101 + + attributes["prevloc"] = prevloc + attributes["previous_la_known"] = previous_la_known + attributes["ppcodenk"] = ppcodenk + attributes["ppostcode_full"] = ppostcode_full + + attributes["reasonpref"] = field_106 + attributes["rp_homeless"] = field_107 + attributes["rp_insan_unsat"] = field_108 + attributes["rp_medwel"] = field_109 + attributes["rp_hardship"] = field_110 + attributes["rp_dontknow"] = field_111 + + attributes["cbl"] = cbl + attributes["chr"] = chr + attributes["cap"] = cap + attributes["accessible_register"] = accessible_register + attributes["letting_allocation_unknown"] = letting_allocation_unknown + + attributes["referral"] = field_116 + + attributes["net_income_known"] = net_income_known + attributes["earnings"] = earnings + attributes["incfreq"] = field_118 + attributes["hb"] = field_120 + attributes["benefits"] = field_121 + + attributes["period"] = field_123 + attributes["brent"] = field_124 if all_charges_given? + attributes["scharge"] = field_125 if all_charges_given? + attributes["pscharge"] = field_126 if all_charges_given? + attributes["supcharg"] = field_127 if all_charges_given? + attributes["household_charge"] = supported_housing? ? field_122 : nil + attributes["hbrentshortfall"] = field_128 + attributes["tshortfall_known"] = tshortfall_known + attributes["tshortfall"] = field_129 + + attributes["hhmemb"] = hhmemb + + attributes["unitletas"] = field_17 + attributes["rsnvac"] = rsnvac + attributes["sheltered"] = field_36 + + attributes["illness_type_1"] = field_94 + attributes["illness_type_2"] = field_88 + attributes["illness_type_3"] = field_91 + attributes["illness_type_4"] = field_86 + attributes["illness_type_5"] = field_87 + attributes["illness_type_6"] = field_89 + attributes["illness_type_7"] = field_90 + attributes["illness_type_8"] = field_93 + attributes["illness_type_9"] = field_92 + attributes["illness_type_10"] = field_95 + + attributes["irproduct_other"] = field_12 + + attributes["propcode"] = field_14 + + attributes["majorrepairs"] = majorrepairs + + attributes["mrcdate"] = mrcdate + + attributes["voiddate"] = voiddate + + attributes["first_time_property_let_as_social_housing"] = first_time_property_let_as_social_housing + + if general_needs? + attributes["uprn_known"] = field_18.present? ? 1 : 0 + attributes["uprn_confirmed"] = 1 if field_18.present? + attributes["skip_update_uprn_confirmed"] = true + attributes["uprn"] = field_18 + attributes["address_line1"] = field_19 + attributes["address_line1_as_entered"] = field_19 + attributes["address_line2"] = field_20 + attributes["address_line2_as_entered"] = field_20 + attributes["town_or_city"] = field_21 + attributes["town_or_city_as_entered"] = field_21 + attributes["county"] = field_22 + attributes["county_as_entered"] = field_22 + attributes["postcode_full"] = postcode_full + attributes["postcode_full_as_entered"] = postcode_full + attributes["postcode_known"] = postcode_known + attributes["la"] = field_25 + attributes["la_as_entered"] = field_25 + + attributes["address_line1_input"] = address_line1_input + attributes["postcode_full_input"] = postcode_full + attributes["select_best_address_match"] = true if field_18.blank? + attributes["manual_address_entry_selected"] = field_18.blank? + end + + attributes + end + + def address_line1_input + [field_19, field_20, field_21].compact.join(", ") + end + + def postcode_known + if postcode_full.present? + 1 + elsif field_25.present? + 0 + end + end + + def postcode_full + [field_23, field_24].compact_blank.join(" ") if field_23 || field_24 + end + + def owning_organisation + Organisation.find_by_id_on_multiple_fields(field_1) + end + + def managing_organisation + Organisation.find_by_id_on_multiple_fields(field_2) + end + + def renewal + case field_7 + when 1 + 1 + when 2 + 0 + else + field_7 + end + end + + def rsnvac + field_16 + end + + def scheme + return if field_5.nil? || owning_organisation.nil? || managing_organisation.nil? + + @scheme ||= Scheme.where(id: (owning_organisation.owned_schemes + managing_organisation.owned_schemes).map(&:id)).find_by_id_on_multiple_fields(field_5.strip, field_6) + end + + def location + return if scheme.nil? + + @location ||= scheme.locations.find_by_id_on_multiple_fields(field_6) + end + + def startdate + year = field_10.to_s.strip.length.between?(1, 2) ? field_10 + 2000 : field_10 + Date.new(year, field_9, field_8) if field_10.present? && field_9.present? && field_8.present? + rescue Date::Error + Date.new + end + + def ethnic_group_from_ethnic + return nil if field_44.blank? + + case field_44 + when 1, 2, 3, 18, 20 + 0 + when 4, 5, 6, 7 + 1 + when 8, 9, 10, 11, 15 + 2 + when 12, 13, 14 + 3 + when 16, 19 + 4 + when 17 + 17 + end + end + + def age1_known? + return 1 if field_42 == "R" + + 0 + end + + [ + { person: 2, field: :field_48 }, + { person: 3, field: :field_52 }, + { person: 4, field: :field_56 }, + { person: 5, field: :field_60 }, + { person: 6, field: :field_64 }, + { person: 7, field: :field_68 }, + { person: 8, field: :field_72 }, + ].each do |hash| + define_method("age#{hash[:person]}_known?") do + return 1 if public_send(hash[:field]) == "R" + return 0 if send("person_#{hash[:person]}_present?") + end + end + + def details_known?(person_n) + send("person_#{person_n}_present?") ? 0 : 1 + end + + def person_2_present? + field_47.present? || field_48.present? || field_49.present? + end + + def person_3_present? + field_51.present? || field_52.present? || field_53.present? + end + + def person_4_present? + field_55.present? || field_56.present? || field_57.present? + end + + def person_5_present? + field_59.present? || field_60.present? || field_61.present? + end + + def person_6_present? + field_63.present? || field_64.present? || field_65.present? + end + + def person_7_present? + field_67.present? || field_68.present? || field_69.present? + end + + def person_8_present? + field_71.present? || field_72.present? || field_73.present? + end + + def leftreg + field_76 + end + + def housingneeds + if field_83 == 1 + 2 + elsif field_84 == 1 + 3 + elsif field_83.blank? || field_83&.zero? + 1 + end + end + + def housingneeds_type + if field_79 == 1 + 0 + elsif field_80 == 1 + 1 + elsif field_81 == 1 + 2 + else + 3 + end + end + + def housingneeds_other + return 1 if field_82 == 1 + return 0 if [field_79, field_80, field_81].include?(1) + end + + def prevloc + field_105 + end + + def previous_la_known + prevloc.present? ? 1 : 0 + end + + def ppcodenk + case field_102 + when 1 + 0 + when 2 + 1 + end + end + + def ppostcode_full + "#{field_103} #{field_104}".strip.gsub(/\s+/, " ") + end + + def cbl + case field_112 + when 2 + 0 + when 1 + 1 + end + end + + def cap + case field_113 + when 2 + 0 + when 1 + 1 + end + end + + def chr + case field_114 + when 2 + 0 + when 1 + 1 + end + end + + def accessible_register + case field_115 + when 2 + 0 + when 1 + 1 + end + end + + def letting_allocation_unknown + [cbl, chr, cap, accessible_register].all?(0) ? 1 : 0 + end + + def net_income_known + case field_117 + when 1 + 0 + when 2 + 1 + when 3 + 2 + end + end + + def earnings + field_119.round if field_119.present? + end + + def tshortfall_known + field_128 == 1 ? 0 : 1 + end + + def hhmemb + [ + person_2_present?, + person_3_present?, + person_4_present?, + person_5_present?, + person_6_present?, + person_7_present?, + person_8_present?, + ].count(true) + 1 + end + + def majorrepairs + mrcdate.present? ? 1 : 0 + end + + def mrcdate + year = field_35.to_s.strip.length.between?(1, 2) ? field_35 + 2000 : field_35 + Date.new(year, field_34, field_33) if field_35.present? && field_34.present? && field_33.present? + rescue Date::Error + Date.new + end + + def voiddate + year = field_32.to_s.strip.length.between?(1, 2) ? field_32 + 2000 : field_32 + Date.new(year, field_31, field_30) if field_32.present? && field_31.present? && field_30.present? + rescue Date::Error + Date.new + end + + def first_time_property_let_as_social_housing + case rsnvac + when 15, 16, 17 + 1 + else + 0 + end + end + + def valid_nationality_options + %w[0] + GlobalConstants::COUNTRIES_ANSWER_OPTIONS.keys # 0 is "Prefers not to say" + end + + def nationality_group(nationality_value) + return unless nationality_value + return 0 if nationality_value.zero? + return 826 if nationality_value == 826 + + 12 + end + + def reason_is_other? + field_98 == 20 + end + + def bulk_upload_organisation + Organisation.find(bulk_upload.organisation_id) + end + + def relationship_from_input_value(value) + case value + when 1 + "P" # yes + when 2 + "X" # no + when 3 + "R" # refused + end + end +end diff --git a/app/services/bulk_upload/sales/log_creator.rb b/app/services/bulk_upload/sales/log_creator.rb index 69f1580a0..a21e7a31a 100644 --- a/app/services/bulk_upload/sales/log_creator.rb +++ b/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 diff --git a/app/services/bulk_upload/sales/validator.rb b/app/services/bulk_upload/sales/validator.rb index 7ad9638d7..0b2d68bc5 100644 --- a/app/services/bulk_upload/sales/validator.rb +++ b/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 diff --git a/app/services/bulk_upload/sales/year2024/row_parser.rb b/app/services/bulk_upload/sales/year2024/row_parser.rb index 443d5e665..6bf1cc4be 100644 --- a/app/services/bulk_upload/sales/year2024/row_parser.rb +++ b/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 diff --git a/app/services/bulk_upload/sales/year2025/csv_parser.rb b/app/services/bulk_upload/sales/year2025/csv_parser.rb new file mode 100644 index 000000000..ec052dbfb --- /dev/null +++ b/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 diff --git a/app/services/bulk_upload/sales/year2025/row_parser.rb b/app/services/bulk_upload/sales/year2025/row_parser.rb new file mode 100644 index 000000000..f6a2a6e1a --- /dev/null +++ b/app/services/bulk_upload/sales/year2025/row_parser.rb @@ -0,0 +1,1503 @@ +class BulkUpload::Sales::Year2025::RowParser + include ActiveModel::Model + include ActiveModel::Attributes + include InterruptionScreenHelper + include FormattingHelper + + QUESTIONS = { + field_1: "What is the day of the sale completion date? - DD", + field_2: "What is the month of the sale completion date? - MM", + field_3: "What is the year of the sale completion date? - YY", + field_4: "Which organisation owned this property before the sale?", + field_5: "Which organisation is reporting this sale?", + field_6: "Username", + field_7: "What is the purchaser code?", + field_8: "What is the sale type?", + field_9: "What is the type of shared ownership sale?", + field_10: "Is this a staircasing transaction?", + field_11: "What is the type of discounted ownership sale?", + field_12: "Is this a joint purchase?", + field_13: "Are there more than two joint purchasers of this property?", + field_14: "Was the buyer interviewed for any of the answers you will provide on this log?", + field_15: "Data Protection question", + + field_16: "If known, enter this property's UPRN", + field_17: "Address line 1", + field_18: "Address line 2", + field_19: "Town or city", + field_20: "County", + field_21: "Part 1 of postcode of property", + field_22: "Part 2 of postcode of property", + field_23: "What is the local authority of the property?", + field_24: "What type of unit is the property?", + field_25: "How many bedrooms does the property have?", + field_26: "Which type of building is the property?", + field_27: "Is the property built or adapted to wheelchair user standards?", + + field_28: "Age of buyer 1", + field_29: "Gender identity of buyer 1", + field_30: "What is buyer 1's ethnic group?", + field_31: "What is buyer 1's nationality?", + field_32: "Working situation of buyer 1", + field_33: "Will buyer 1 live in the property?", + field_34: "Is buyer 2 or person 2 the partner of buyer 1?", + field_35: "Age of person 2", + field_36: "Gender identity of person 2", + field_37: "Which of the following best describes buyer 2's ethnic background?", + field_38: "What is buyer 2's nationality?", + field_39: "What is buyer 2 or person 2's working situation?", + field_40: "Will buyer 2 live in the property?", + field_41: "Besides the buyers, how many people will live in the property?", + + field_42: "Is person 3 the partner of buyer 1?", + field_43: "Age of person 3", + field_44: "Gender identity of person 3", + field_45: "Working situation of person 3", + field_46: "Is person 4 the partner of buyer 1?", + field_47: "Age of person 4", + field_48: "Gender identity of person 4", + field_49: "Working situation of person 4", + field_50: "Is person 5 the partner of buyer 1?", + field_51: "Age of person 5", + field_52: "Gender identity of person 5", + field_53: "Working situation of person 5", + field_54: "Is person 6 the partner of buyer 1?", + field_55: "Age of person 6", + field_56: "Gender identity of person 6", + field_57: "Working situation of person 6", + + field_58: "What was buyer 1's previous tenure?", + field_59: "Do you know the postcode of buyer 1's last settled home?", + field_60: "Part 1 of postcode of buyer 1's last settled home", + field_61: "Part 2 of postcode of buyer 1's last settled home", + field_62: "What is the local authority of buyer 1's last settled home?", + field_63: "At the time of purchase, was buyer 2 living at the same address as buyer 1?", + field_64: "What was buyer 2's previous tenure?", + + field_65: "Has the buyer ever served in the UK Armed Forces and for how long?", + field_66: "Is the buyer still serving in the UK armed forces?", + field_67: "Are any of the buyers a spouse or civil partner of a UK Armed Forces regular who died in service within the last 2 years?", + field_68: "Does anyone in the household consider themselves to have a disability?", + field_69: "Does anyone in the household use a wheelchair?", + + field_70: "What is buyer 1's gross annual income?", + field_71: "Was buyer 1's income used for a mortgage application?", + field_72: "What is buyer 2's gross annual income?", + field_73: "Was buyer 2's income used for a mortgage application?", + field_74: "Were the buyers receiving any of these housing-related benefits immediately before buying this property?", + field_75: "What is the total amount the buyers had in savings before they paid any deposit for the property?", + field_76: "Have any of the purchasers previously owned a property?", + field_77: "Was the previous property under shared ownership?", + + field_78: "Is this a resale?", + field_79: "How long have the buyers been living in the property before the purchase? - Shared ownership", + field_80: "What is the day of the practical completion or handover date?", + field_81: "What is the month of the practical completion or handover date?", + field_82: "What is the year of the practical completion or handover date?", + field_83: "How many bedrooms did the buyer's previous property have?", + field_84: "What was the type of the buyer's previous property?", + field_85: "What was the rent type of the buyer's previous property?", + field_86: "What was the full purchase price?", + field_87: "What was the initial percentage share purchased?", + field_88: "Was a mortgage used for the purchase of this property? - Shared ownership", + field_89: "What is the mortgage amount?", + field_90: "What is the length of the mortgage in years? - Shared ownership", + field_91: "How much was the cash deposit paid on the property?", + field_92: "How much cash discount was given through Social Homebuy?", + field_93: "What is the basic monthly rent?", + field_94: "What are the total monthly service charges for the property?", + field_95: "What are the total monthly estate management fees for the property?", + + field_96: "What percentage of the property has been bought in this staircasing transaction?", + field_97: "What percentage of the property does the buyer now own in total?", + field_98: "Was this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?", + field_99: "Is this the first time the buyer has engaged in staircasing in the home?", + field_100: "What was the day of the initial purchase of a share in the property? DD", + field_101: "What was the month of the initial purchase of a share in the property? MM", + field_102: "What was the year of the initial purchase of a share in the property? YYYY", + field_103: "Including this time, how many times has the shared owner engaged in staircasing in the home?", + field_104: "What was the day of the last staircasing transaction? DD", + field_105: "What was the month of the last staircasing transaction? MM", + field_106: "What was the year of the last staircasing transaction? YYYY", + field_107: "What is the full purchase price for this staircasing transaction?", + field_108: "What was the percentage share purchased in the initial transaction?", + field_109: "Was a mortgage used for this staircasing transaction?", + field_110: "What was the basic monthly rent prior to staircasing?", + field_111: "What is the basic monthly rent after staircasing?", + + field_112: "How long have the buyers been living in the property before the purchase? - Discounted ownership", + field_113: "What was the full purchase price?", + field_114: "What was the amount of any loan, grant, discount or subsidy given?", + field_115: "What was the percentage discount?", + field_116: "Was a mortgage used for the purchase of this property? - Discounted ownership", + field_117: "What is the mortgage amount?", + field_118: "What is the length of the mortgage in years? - Discounted ownership", + field_119: "Does this include any extra borrowing?", + field_120: "How much was the cash deposit paid on the property?", + field_121: "What are the total monthly leasehold charges for the property?", + }.freeze + + ERROR_BASE_KEY = "validations.sales.2025.bulk_upload".freeze + + attribute :bulk_upload + attribute :block_log_creation, :boolean, default: -> { false } + + attribute :field_blank + + attribute :field_1, :integer + attribute :field_2, :integer + attribute :field_3, :integer + attribute :field_4, :string + attribute :field_5, :string + attribute :field_6, :string + attribute :field_7, :string + attribute :field_8, :integer + attribute :field_9, :integer + attribute :field_10, :integer + attribute :field_11, :integer + attribute :field_12, :integer + attribute :field_13, :integer + attribute :field_14, :integer + attribute :field_15, :integer + + attribute :field_16, :string + attribute :field_17, :string + attribute :field_18, :string + attribute :field_19, :string + attribute :field_20, :string + attribute :field_21, :string + attribute :field_22, :string + attribute :field_23, :string + attribute :field_24, :integer + attribute :field_25, :integer + attribute :field_26, :integer + attribute :field_27, :integer + + attribute :field_28, :string + attribute :field_29, :string + attribute :field_30, :integer + attribute :field_31, :integer + attribute :field_32, :integer + attribute :field_33, :integer + attribute :field_34, :integer + attribute :field_35, :string + attribute :field_36, :string + attribute :field_37, :integer + attribute :field_38, :integer + attribute :field_39, :integer + attribute :field_40, :integer + attribute :field_41, :integer + + attribute :field_42, :integer + attribute :field_43, :string + attribute :field_44, :string + attribute :field_45, :integer + attribute :field_46, :integer + attribute :field_47, :string + attribute :field_48, :string + attribute :field_49, :integer + attribute :field_50, :integer + attribute :field_51, :string + attribute :field_52, :string + attribute :field_53, :integer + attribute :field_54, :integer + attribute :field_55, :string + attribute :field_56, :string + attribute :field_57, :integer + + attribute :field_58, :integer + attribute :field_59, :integer + attribute :field_60, :string + attribute :field_61, :string + attribute :field_62, :string + attribute :field_63, :integer + attribute :field_64, :string + + attribute :field_65, :integer + attribute :field_66, :integer + attribute :field_67, :integer + attribute :field_68, :integer + attribute :field_69, :integer + + attribute :field_70, :string + attribute :field_71, :integer + attribute :field_72, :string + attribute :field_73, :integer + attribute :field_74, :integer + attribute :field_75, :string + attribute :field_76, :integer + attribute :field_77, :integer + + attribute :field_78, :integer + attribute :field_79, :integer + attribute :field_80, :integer + attribute :field_81, :integer + attribute :field_82, :integer + attribute :field_83, :integer + attribute :field_84, :integer + attribute :field_85, :integer + attribute :field_86, :integer + attribute :field_87, :integer + attribute :field_88, :integer + attribute :field_89, :integer + attribute :field_90, :integer + attribute :field_91, :integer + attribute :field_92, :integer + attribute :field_93, :decimal + attribute :field_94, :decimal + attribute :field_95, :decimal + + attribute :field_96, :integer + attribute :field_97, :integer + attribute :field_98, :integer + attribute :field_99, :integer + attribute :field_100, :integer + attribute :field_101, :integer + attribute :field_102, :integer + attribute :field_103, :integer + attribute :field_104, :integer + attribute :field_105, :integer + attribute :field_106, :integer + attribute :field_107, :integer + attribute :field_108, :integer + attribute :field_109, :integer + attribute :field_110, :integer + attribute :field_111, :decimal + + attribute :field_112, :integer + attribute :field_113, :integer + attribute :field_114, :integer + attribute :field_115, :integer + attribute :field_116, :integer + attribute :field_117, :integer + attribute :field_118, :integer + attribute :field_119, :integer + attribute :field_120, :integer + attribute :field_121, :integer + + validates :field_1, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (day)."), + category: :setup, + }, + on: :after_log + + validates :field_2, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (month)."), + category: :setup, + }, on: :after_log + + validates :field_3, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "sale completion date (year)."), + category: :setup, + }, + format: { + with: /\A(\d{2}|\d{4})\z/, + message: I18n.t("#{ERROR_BASE_KEY}.saledate.year_not_two_or_four_digits"), + category: :setup, + if: proc { field_3.present? }, + }, on: :after_log + + validates :field_8, + inclusion: { + in: [1, 2], + if: proc { field_8.present? }, + category: :setup, + question: QUESTIONS[:field_8].downcase, + }, + on: :before_log + + validates :field_8, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "shared ownership sale type."), + category: :setup, + }, + on: :after_log + + validates :field_9, + inclusion: { + in: [2, 30, 18, 16, 24, 28, 31, 32], + if: proc { field_9.present? }, + category: :setup, + question: QUESTIONS[:field_9].downcase, + }, + on: :before_log + + validates :field_9, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "type of shared ownership sale."), + category: :setup, + if: :shared_ownership?, + }, + on: :after_log + + validates :field_10, + inclusion: { + in: [1, 2], + if: proc { field_10.present? }, + category: :setup, + question: QUESTIONS[:field_10].downcase, + }, + on: :before_log + + validates :field_10, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "staircasing transaction."), + category: :setup, + if: :shared_ownership?, + }, + on: :after_log + + validates :field_11, + inclusion: { + in: [8, 9, 14, 21, 22, 27, 29], + if: proc { field_11.present? }, + category: :setup, + question: QUESTIONS[:field_11].downcase, + }, + on: :before_log + + validates :field_11, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "type of discounted ownership sale."), + category: :setup, + if: :discounted_ownership?, + }, + on: :after_log + + validates :field_115, + numericality: { + message: I18n.t("#{ERROR_BASE_KEY}.numeric.within_range", field: "Percentage discount", min: "0%", max: "70%"), + greater_than_or_equal_to: 0, + less_than_or_equal_to: 70, + if: :discounted_ownership?, + allow_blank: true, + }, + on: :before_log + + validates :field_12, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "joint purchase."), + category: :setup, + if: :joint_purchase_asked?, + }, + on: :after_log + + validates :field_13, + presence: { + message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "more than 2 joint buyers."), + category: :setup, + if: :joint_purchase?, + }, + on: :after_log + + validate :validate_buyer1_economic_status, on: :before_log + validate :validate_address_option_found, on: :after_log + validate :validate_buyer2_economic_status, on: :before_log + validate :validate_valid_radio_option, on: :before_log + + validate :validate_owning_org_data_given, on: :after_log + validate :validate_owning_org_exists, on: :after_log + validate :validate_owning_org_owns_stock, on: :after_log + validate :validate_owning_org_permitted, on: :after_log + + validate :validate_assigned_to_exists, on: :after_log + validate :validate_assigned_to_related, on: :after_log + validate :validate_assigned_to_when_support, on: :after_log + validate :validate_managing_org_related, on: :after_log + validate :validate_relevant_collection_window, on: :after_log + validate :validate_incomplete_soft_validations, on: :after_log + + validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log + validate :validate_address_fields, on: :after_log + validate :validate_if_log_already_exists, on: :after_log, if: -> { FeatureToggle.bulk_upload_duplicate_log_check_enabled? } + + validate :validate_nationality, on: :after_log + validate :validate_buyer_2_nationality, on: :after_log + + validate :validate_nulls, on: :after_log + + def self.question_for_field(field) + QUESTIONS[field] + end + + def attribute_set + @attribute_set ||= instance_variable_get(:@attributes) + end + + def blank_row? + attribute_set + .to_hash + .reject { |k, _| %w[bulk_upload block_log_creation].include?(k) } + .values + .reject(&:blank?) + .compact + .empty? + end + + def log + @log ||= SalesLog.new(attributes_for_log) + end + + def valid? + errors.clear + + return true if blank_row? + + super(:before_log) + @before_errors = errors.dup + + log.valid? + + super(:after_log) + errors.merge!(@before_errors) + + log.errors.each do |error| + fields = field_mapping_for_errors[error.attribute] || [] + + fields.each do |field| + next if errors.include?(field) + next if error.type == :skip_bu_error + + question = log.form.get_question(error.attribute, log) + + if question.present? && setup_question?(question) + errors.add(field, error.message, category: :setup) + else + errors.add(field, error.message) + end + end + end + + errors.blank? + end + + def block_log_creation? + block_log_creation + end + + def inspect + "#" + end + + def log_already_exists? + return false if blank_row? + + @log_already_exists ||= SalesLog + .where(status: %w[not_started in_progress completed]) + .exists?(duplicate_check_fields.index_with { |field| log.public_send(field) }) + end + + def purchaser_code + field_7 + end + + def spreadsheet_duplicate_hash + attributes.slice( + "field_4", # owning org + "field_1", # saledate + "field_2", # saledate + "field_3", # saledate + "field_7", # purchaser_code + "field_21", # postcode + "field_22", # postcode + "field_28", # age1 + "field_29", # sex1 + "field_32", # ecstat1 + ) + end + + def add_duplicate_found_in_spreadsheet_errors + spreadsheet_duplicate_hash.each_key do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.spreadsheet_dupe"), category: :setup) + end + end + +private + + def prevtenbuy2 + case field_64 + when "R" + 0 + else + field_64 + end + end + + def infer_buyer2_ethnic_group_from_ethnic + case field_37 + when 1, 2, 3, 18, 20 + 0 + when 4, 5, 6, 7 + 1 + when 8, 9, 10, 11, 15 + 2 + when 12, 13, 14 + 3 + when 16, 19 + 4 + else + field_37 + end + end + + def validate_uprn_exists_if_any_key_address_fields_are_blank + if field_16.blank? && !key_address_fields_provided? + %i[field_17 field_19 field_21 field_22].each do |field| + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.address.not_answered")) if send(field).blank? + end + errors.add(:field_16, I18n.t("#{ERROR_BASE_KEY}.address.not_answered", question: "UPRN.")) + end + end + + def validate_address_option_found + if log.uprn.nil? && field_16.blank? && key_address_fields_provided? + error_message = if log.address_options_present? + I18n.t("#{ERROR_BASE_KEY}.address.not_determined") + else + I18n.t("#{ERROR_BASE_KEY}.address.not_found") + end + %i[field_17 field_18 field_19 field_20 field_21 field_22].each do |field| + errors.add(field, error_message) if errors[field].blank? + end + end + end + + def key_address_fields_provided? + field_17.present? && field_19.present? && postcode_full.present? + end + + def validate_address_fields + if field_16.blank? || log.errors.attribute_names.include?(:uprn) + if field_17.blank? && errors[:field_17].blank? + errors.add(:field_17, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "address line 1.")) + end + + if field_19.blank? && errors[:field_19].blank? + errors.add(:field_19, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "town or city.")) + end + + if field_21.blank? && errors[:field_21].blank? + errors.add(:field_21, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 1 of postcode.")) + end + + if field_22.blank? && errors[:field_22].blank? + errors.add(:field_22, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode.")) + end + end + end + + def shared_ownership? + field_8 == 1 + end + + def discounted_ownership? + field_8 == 2 + end + + def joint_purchase? + field_12 == 1 + end + + def joint_purchase_asked? + shared_ownership? || discounted_ownership? || field_13 == 2 + end + + def shared_or_discounted_but_not_staircasing? + (shared_ownership? || discounted_ownership?) && field_10 != 1 + end + + def shared_ownership_initial_purchase? + field_8 == 1 && field_10 != 1 + end + + def staircasing? + field_8 == 1 && field_10 == 1 + end + + def two_buyers_share_address? + field_63 == 2 + end + + def not_resale? + field_78 == 2 + end + + def buyer_1_previous_tenure_not_1_or_2? + field_58 != 1 && field_58 != 2 + end + + def mortgage_used? + field_88 == 2 + end + + def social_homebuy? + field_9 == 18 + end + + def buyers_own_all? + field_97 == 100 + end + + def buyer_staircased_before? + field_99 == 1 + end + + def rtb_like_sale_type? + [9, 14, 27, 29].include?(field_11) + end + + def field_mapping_for_errors + { + purchid: %i[field_7], + saledate: %i[field_1 field_2 field_3], + noint: %i[field_14], + age1_known: %i[field_28], + age1: %i[field_28], + age2_known: %i[field_35], + age2: %i[field_35], + age3_known: %i[field_43], + age3: %i[field_43], + age4_known: %i[field_47], + age4: %i[field_47], + age5_known: %i[field_51], + age5: %i[field_51], + age6_known: %i[field_55], + age6: %i[field_55], + sex1: %i[field_29], + sex2: %i[field_36], + sex3: %i[field_44], + + sex4: %i[field_48], + sex5: %i[field_52], + sex6: %i[field_56], + relat2: %i[field_34], + relat3: %i[field_42], + relat4: %i[field_46], + relat5: %i[field_49], + relat6: %i[field_54], + + ecstat1: %i[field_32], + ecstat2: %i[field_39], + ecstat3: %i[field_45], + + ecstat4: %i[field_49], + ecstat5: %i[field_53], + ecstat6: %i[field_57], + ethnic_group: %i[field_30], + ethnic: %i[field_30], + nationality_all: %i[field_31], + nationality_all_group: %i[field_31], + income1nk: %i[field_70], + income1: %i[field_70], + income2nk: %i[field_72], + income2: %i[field_72], + inc1mort: %i[field_71], + inc2mort: %i[field_73], + savingsnk: %i[field_75], + savings: %i[field_75], + prevown: %i[field_76], + prevten: %i[field_58], + prevloc: %i[field_62], + previous_la_known: %i[field_62], + ppcodenk: %i[field_59], + ppostcode_full: %i[field_60 field_61], + disabled: %i[field_68], + + wheel: %i[field_69], + beds: %i[field_25], + proptype: %i[field_24], + builtype: %i[field_26], + la_known: %i[field_23], + la: %i[field_23], + + is_la_inferred: %i[field_23], + pcodenk: %i[field_21 field_22], + postcode_full: %i[field_21 field_22], + wchair: %i[field_27], + + type: %i[field_9 field_11 field_8], + resale: %i[field_78], + hodate: %i[field_80 field_81 field_82], + + frombeds: %i[field_83], + fromprop: %i[field_84], + value: value_fields, + equity: equity_fields, + mortgage: mortgage_fields, + extrabor: extrabor_fields, + deposit: deposit_fields, + cashdis: %i[field_92], + mrent: mrent_fields, + + has_mscharge: mscharge_fields, + mscharge: mscharge_fields, + grant: %i[field_114], + discount: %i[field_115], + owning_organisation_id: %i[field_4], + managing_organisation_id: [:field_5], + assigned_to: %i[field_6], + hhregres: %i[field_65], + hhregresstill: %i[field_66], + armedforcesspouse: %i[field_67], + + hb: %i[field_74], + mortlen: mortlen_fields, + proplen: proplen_fields, + + jointmore: %i[field_13], + staircase: %i[field_10], + privacynotice: %i[field_15], + ownershipsch: %i[field_8], + + jointpur: %i[field_12], + buy1livein: %i[field_33], + buy2livein: %i[field_40], + hholdcount: %i[field_41], + stairbought: %i[field_96], + stairowned: %i[field_97], + socprevten: %i[field_85], + mortgageused: mortgageused_fields, + + uprn: %i[field_16], + address_line1: %i[field_17], + address_line2: %i[field_18], + town_or_city: %i[field_19], + county: %i[field_20], + uprn_selection: [:field_17], + + ethnic_group2: %i[field_37], + ethnicbuy2: %i[field_37], + nationality_all_buyer2: %i[field_38], + nationality_all_buyer2_group: %i[field_38], + + buy2living: %i[field_63], + prevtenbuy2: %i[field_64], + + prevshared: %i[field_77], + + staircasesale: %i[field_98], + firststair: %i[field_99], + numstair: %i[field_103], + mrentprestaircasing: %i[field_110], + lasttransaction: %i[field_104 field_105 field_106], + initialpurchase: %i[field_100 field_101 field_102], + + } + end + + def attributes_for_log + attributes = {} + + attributes["purchid"] = purchaser_code + attributes["saledate"] = saledate + attributes["noint"] = field_14 + + attributes["age1_known"] = age1_known? + attributes["age1"] = field_28 if attributes["age1_known"]&.zero? && field_28&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age2_known"] = age2_known? + attributes["age2"] = field_35 if attributes["age2_known"]&.zero? && field_35&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age3_known"] = age3_known? + attributes["age3"] = field_43 if attributes["age3_known"]&.zero? && field_43&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age4_known"] = age4_known? + attributes["age4"] = field_47 if attributes["age4_known"]&.zero? && field_47&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age5_known"] = age5_known? + attributes["age5"] = field_51 if attributes["age5_known"]&.zero? && field_51&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age6_known"] = age6_known? + attributes["age6"] = field_55 if attributes["age6_known"]&.zero? && field_55&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["sex1"] = field_29 + attributes["sex2"] = field_36 + attributes["sex3"] = field_44 + attributes["sex4"] = field_48 + attributes["sex5"] = field_52 + attributes["sex6"] = field_56 + + attributes["relat2"] = if field_34 == 1 + "P" + else + (field_34 == 2 ? "X" : "R") + end + attributes["relat3"] = if field_42 == 1 + "P" + else + (field_42 == 2 ? "X" : "R") + end + attributes["relat4"] = if field_46 == 1 + "P" + else + (field_46 == 2 ? "X" : "R") + end + attributes["relat5"] = if field_49 == 1 + "P" + else + (field_49 == 2 ? "X" : "R") + end + attributes["relat6"] = if field_54 == 1 + "P" + else + (field_54 == 2 ? "X" : "R") + end + + attributes["ecstat1"] = field_32 + attributes["ecstat2"] = field_39 + attributes["ecstat3"] = field_45 + attributes["ecstat4"] = field_49 + attributes["ecstat5"] = field_53 + attributes["ecstat6"] = field_57 + + attributes["details_known_2"] = details_known?(2) + attributes["details_known_3"] = details_known?(3) + attributes["details_known_4"] = details_known?(4) + attributes["details_known_5"] = details_known?(5) + attributes["details_known_6"] = details_known?(6) + + attributes["ethnic_group"] = ethnic_group_from_ethnic + attributes["ethnic"] = field_30 + attributes["nationality_all"] = field_31 if field_31.present? && valid_nationality_options.include?(field_31.to_s) + attributes["nationality_all_group"] = nationality_group(attributes["nationality_all"]) + + attributes["income1nk"] = field_70 == "R" ? 1 : 0 + attributes["income1"] = field_70.to_i if attributes["income1nk"]&.zero? && field_70&.match(/\A\d+\z/) + + attributes["income2nk"] = field_72 == "R" ? 1 : 0 + attributes["income2"] = field_72.to_i if attributes["income2nk"]&.zero? && field_72&.match(/\A\d+\z/) + + attributes["inc1mort"] = field_71 + attributes["inc2mort"] = field_73 + + attributes["savingsnk"] = field_75 == "R" ? 1 : 0 + attributes["savings"] = field_75.to_i if attributes["savingsnk"]&.zero? && field_75&.match(/\A\d+\z/) + attributes["prevown"] = field_76 + + attributes["prevten"] = field_58 + attributes["prevloc"] = field_62 + attributes["previous_la_known"] = previous_la_known + attributes["ppcodenk"] = previous_postcode_known + attributes["ppostcode_full"] = ppostcode_full + + attributes["disabled"] = field_68 + attributes["wheel"] = field_69 + attributes["beds"] = field_25 + attributes["proptype"] = field_24 + attributes["builtype"] = field_26 + attributes["la_known"] = field_23.present? ? 1 : 0 + attributes["la"] = field_23 + attributes["la_as_entered"] = field_23 + attributes["is_la_inferred"] = false + attributes["pcodenk"] = 0 if postcode_full.present? + attributes["postcode_full"] = postcode_full + attributes["postcode_full_as_entered"] = postcode_full + attributes["wchair"] = field_27 + + attributes["type"] = sale_type + attributes["resale"] = field_78 + + attributes["hodate"] = hodate + + attributes["frombeds"] = field_83 + attributes["fromprop"] = field_84 + + attributes["value"] = value + attributes["equity"] = equity + attributes["mortgage"] = mortgage + attributes["extrabor"] = extrabor + attributes["deposit"] = deposit + + attributes["cashdis"] = field_92 + attributes["mrent"] = mrent + attributes["mscharge"] = mscharge if mscharge&.positive? + attributes["has_mscharge"] = attributes["mscharge"].present? ? 1 : 0 + attributes["grant"] = field_114 + attributes["discount"] = field_115 + + attributes["owning_organisation"] = owning_organisation + attributes["managing_organisation"] = managing_organisation + attributes["assigned_to"] = assigned_to || (bulk_upload.user.support? ? nil : bulk_upload.user) + attributes["created_by"] = bulk_upload.user + attributes["hhregres"] = field_65 + attributes["hhregresstill"] = field_66 + attributes["armedforcesspouse"] = field_67 + + attributes["hb"] = field_74 + + attributes["mortlen"] = mortlen + + attributes["proplen"] = proplen if proplen&.positive? + attributes["proplen_asked"] = attributes["proplen"]&.present? ? 0 : 1 + attributes["jointmore"] = field_13 + attributes["staircase"] = field_10 + attributes["privacynotice"] = field_15 + attributes["ownershipsch"] = field_8 + attributes["jointpur"] = field_12 + attributes["buy1livein"] = field_33 + attributes["buy2livein"] = field_40 + attributes["hholdcount"] = field_41 + attributes["stairbought"] = field_96 + attributes["stairowned"] = field_97 + attributes["socprevten"] = field_85 + attributes["soctenant"] = infer_soctenant_from_prevten_and_prevtenbuy2 + attributes["mortgageused"] = mortgageused + + attributes["uprn"] = field_16 + attributes["uprn_known"] = field_16.present? ? 1 : 0 + attributes["uprn_confirmed"] = 1 if field_16.present? + attributes["skip_update_uprn_confirmed"] = true + attributes["address_line1"] = field_17 + attributes["address_line1_as_entered"] = field_17 + attributes["address_line2"] = field_18 + attributes["address_line2_as_entered"] = field_18 + attributes["town_or_city"] = field_19 + attributes["town_or_city_as_entered"] = field_19 + attributes["county"] = field_20 + attributes["county_as_entered"] = field_20 + 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? + + attributes["ethnic_group2"] = infer_buyer2_ethnic_group_from_ethnic + attributes["ethnicbuy2"] = field_37 + attributes["nationality_all_buyer2"] = field_38 if field_38.present? && valid_nationality_options.include?(field_38.to_s) + attributes["nationality_all_buyer2_group"] = nationality_group(attributes["nationality_all_buyer2"]) + + attributes["buy2living"] = field_63 + attributes["prevtenbuy2"] = prevtenbuy2 + + attributes["prevshared"] = field_77 + + attributes["staircasesale"] = field_98 + + attributes["firststair"] = field_99 + attributes["numstair"] = field_103 + attributes["mrentprestaircasing"] = field_110 + attributes["lasttransaction"] = lasttransaction + attributes["initialpurchase"] = initialpurchase + + attributes["management_fee"] = field_95 + attributes["has_management_fee"] = field_95.present? && field_95.positive? ? 1 : 0 + + attributes + end + + def address_line1_input + [field_17, field_18, field_19].compact.join(", ") + end + + def saledate + year = field_3.to_s.strip.length.between?(1, 2) ? field_3 + 2000 : field_3 + Date.new(year, field_2, field_1) if field_3.present? && field_2.present? && field_1.present? + rescue Date::Error + Date.new + end + + def hodate + year = field_82.to_s.strip.length.between?(1, 2) ? field_82 + 2000 : field_82 + Date.new(year, field_81, field_80) if field_82.present? && field_81.present? && field_80.present? + rescue Date::Error + Date.new + end + + def lasttransaction + year = field_106.to_s.strip.length.between?(1, 2) ? field_106 + 2000 : field_106 + Date.new(year, field_105, field_104) if field_106.present? && field_105.present? && field_104.present? + rescue Date::Error + Date.new + end + + def initialpurchase + year = field_102.to_s.strip.length.between?(1, 2) ? field_102 + 2000 : field_102 + Date.new(year, field_101, field_100) if field_102.present? && field_101.present? && field_100.present? + rescue Date::Error + Date.new + end + + def age1_known? + return 1 if field_28 == "R" + + 0 + end + + [ + { person: 2, field: :field_35 }, + { person: 3, field: :field_43 }, + { person: 4, field: :field_47 }, + { person: 5, field: :field_51 }, + { person: 6, field: :field_55 }, + ].each do |hash| + define_method("age#{hash[:person]}_known?") do + return 1 if public_send(hash[:field]) == "R" + return 0 if send("person_#{hash[:person]}_present?") + end + end + + def person_2_present? + field_35.present? || field_36.present? || field_34.present? + end + + def person_3_present? + field_43.present? || field_44.present? || field_42.present? + end + + def person_4_present? + field_47.present? || field_48.present? || field_46.present? + end + + def person_5_present? + field_51.present? || field_52.present? || field_49.present? + end + + def person_6_present? + field_55.present? || field_56.present? || field_54.present? + end + + def details_known?(person_n) + send("person_#{person_n}_present?") ? 1 : 2 + end + + def ethnic_group_from_ethnic + return nil if field_30.blank? + + case field_30 + when 1, 2, 3, 18, 20 + 0 + when 4, 5, 6, 7 + 1 + when 8, 9, 10, 11, 15 + 2 + when 12, 13, 14 + 3 + when 16, 19 + 4 + when 17 + 17 + end + end + + def postcode_full + [field_21, field_22].compact_blank.join(" ") if field_21 || field_22 + end + + def ppostcode_full + "#{field_60} #{field_61}" if field_60 && field_61 + end + + def sale_type + return field_9 if shared_ownership? + return field_11 if discounted_ownership? + end + + def value + return field_86 if shared_ownership_initial_purchase? + return field_113 if discounted_ownership? + return field_107 if staircasing? + end + + def equity + return field_87 if shared_ownership_initial_purchase? + return field_108 if staircasing? + end + + def mortgage + return field_89 if shared_ownership? + return field_117 if discounted_ownership? + end + + def extrabor + return field_119 if discounted_ownership? + end + + def deposit + return field_91 if shared_ownership? + return field_120 if discounted_ownership? + end + + def mrent + return field_93 if shared_ownership_initial_purchase? + return field_111 if staircasing? + end + + def mscharge + return field_94 if shared_ownership? + return field_121 if discounted_ownership? + end + + def mortlen + return field_90 if shared_ownership? + return field_118 if discounted_ownership? + end + + def proplen + return field_79 if shared_ownership? + return field_112 if discounted_ownership? + end + + def mortgageused + return field_88 if shared_ownership_initial_purchase? + return field_116 if discounted_ownership? + return field_109 if staircasing? + end + + def value_fields + return [:field_86] if shared_ownership_initial_purchase? + return [:field_113] if discounted_ownership? + return [:field_107] if staircasing? + + %i[field_86 field_113 field_107] + end + + def equity_fields + return [:field_87] if shared_ownership_initial_purchase? + return [:field_108] if staircasing? + + %i[field_87 field_108] + end + + def mortgage_fields + return [:field_89] if shared_ownership? + return [:field_117] if discounted_ownership? + + %i[field_89 field_117] + end + + def extrabor_fields + return [:field_119] if discounted_ownership? + + %i[field_119] + end + + def deposit_fields + return [:field_91] if shared_ownership? + return [:field_120] if discounted_ownership? + + %i[field_91 field_120] + end + + def mrent_fields + return [:field_93] if shared_ownership_initial_purchase? + return [:field_111] if staircasing? + + %i[field_93 field_111] + end + + def mscharge_fields + return [:field_94] if shared_ownership? + return [:field_121] if discounted_ownership? + + %i[field_94 field_121] + end + + def mortlen_fields + return [:field_90] if shared_ownership? + return [:field_118] if discounted_ownership? + + %i[field_90 field_118] + end + + def proplen_fields + return [:field_79] if shared_ownership? + return [:field_112] if discounted_ownership? + + %i[field_79 field_112] + end + + def mortgageused_fields + return [:field_88] if shared_ownership_initial_purchase? + return [:field_116] if discounted_ownership? + return [:field_109] if staircasing? + + %i[field_88 field_116 field_109] + end + + def owning_organisation + @owning_organisation ||= Organisation.find_by_id_on_multiple_fields(field_4) + end + + def assigned_to + @assigned_to ||= User.where("lower(email) = ?", field_6&.downcase).first + end + + def previous_la_known + field_62.present? ? 1 : 0 + end + + def previous_postcode_known + return 1 if field_59 == 2 + + 0 if field_59 == 1 + end + + def infer_soctenant_from_prevten_and_prevtenbuy2 + return unless shared_ownership? + + if [1, 2].include?(field_58) || [1, 2].include?(field_64.to_i) + 1 + else + 2 + end + end + + def block_log_creation! + self.block_log_creation = true + end + + def questions + @questions ||= log.form.subsections.flat_map { |ss| ss.applicable_questions(log) } + end + + def duplicate_check_fields + %w[ + saledate + age1 + sex1 + ecstat1 + owning_organisation + postcode_full + purchid + ] + end + + def validate_owning_org_data_given + if field_4.blank? + block_log_creation! + + if errors[:field_4].blank? + errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "owning organisation."), category: :setup) + end + end + end + + def validate_owning_org_exists + if owning_organisation.nil? + block_log_creation! + + if field_4.present? && errors[:field_4].blank? + errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_found"), category: :setup) + end + end + end + + def validate_owning_org_owns_stock + if owning_organisation && !owning_organisation.holds_own_stock? + block_log_creation! + + if errors[:field_4].blank? + errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_stock_owner"), category: :setup) + end + end + end + + def validate_owning_org_permitted + return unless owning_organisation + return if bulk_upload_organisation.affiliated_stock_owners.include?(owning_organisation) + + block_log_creation! + + return if errors[:field_4].present? + + if bulk_upload.user.support? + errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_permitted.support", name: bulk_upload_organisation.name), category: :setup) + else + errors.add(:field_4, I18n.t("#{ERROR_BASE_KEY}.owning_organisation.not_permitted.not_support"), category: :setup) + end + end + + def validate_assigned_to_exists + return if field_6.blank? + + unless assigned_to + errors.add(:field_6, I18n.t("#{ERROR_BASE_KEY}.assigned_to.not_found")) + end + end + + def validate_assigned_to_when_support + if field_6.blank? && bulk_upload.user.support? + errors.add(:field_6, category: :setup, message: I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "what is the CORE username of the account this sales log should be assigned to?")) + end + end + + def validate_assigned_to_related + return unless assigned_to + return if assigned_to.organisation == owning_organisation || assigned_to.organisation == managing_organisation + return if assigned_to.organisation == owning_organisation&.absorbing_organisation || assigned_to.organisation == managing_organisation&.absorbing_organisation + + block_log_creation! + errors.add(:field_6, I18n.t("#{ERROR_BASE_KEY}.assigned_to.organisation_not_related"), category: :setup) + end + + def managing_organisation + Organisation.find_by_id_on_multiple_fields(field_5) + end + + def nationality_group(nationality_value) + return unless nationality_value + return 0 if nationality_value.zero? + return 826 if nationality_value == 826 + + 12 + end + + def validate_managing_org_related + if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation) + block_log_creation! + + if errors[:field_5].blank? + errors.add(:field_5, I18n.t("#{ERROR_BASE_KEY}.assigned_to.managing_organisation_not_related"), category: :setup) + end + end + end + + def setup_question?(question) + log.form.setup_sections[0].subsections[0].questions.include?(question) + end + + def validate_nulls + field_mapping_for_errors.each do |error_key, fields| + question_id = error_key.to_s + question = questions.find { |q| q.id == question_id } + + next unless question + next if log.optional_fields.include?(question.id) + next if question.completed?(log) + + if setup_question?(question) + fields.each do |field| + if errors.none? { |e| fields.include?(e.attribute) } && @before_errors.none? { |e| fields.include?(e.attribute) } + errors.add(field, question.unanswered_error_message, category: :setup) + end + end + else + fields.each do |field| + if errors.none? { |e| fields.include?(e.attribute) } && @before_errors.none? { |e| fields.include?(e.attribute) } + errors.add(field, question.unanswered_error_message) + end + end + end + end + end + + def validate_valid_radio_option + log.attributes.each do |question_id, _v| + question = log.form.get_question(question_id, log) + + next if question_id == "type" + + next unless question&.type == "radio" + next if log[question_id].blank? || question.answer_options.key?(log[question_id].to_s) || !question.page.routed_to?(log, nil) + + fields = field_mapping_for_errors[question_id.to_sym] || [] + + if setup_question?(question) + fields.each do |field| + if errors[field].none? + block_log_creation! + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field])), category: :setup) + end + end + else + fields.each do |field| + unless errors.any? { |e| fields.include?(e.attribute) } + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field]))) + end + end + end + end + end + + def validate_relevant_collection_window + return if saledate.blank? || bulk_upload.form.blank? + return if errors.key?(:field_1) || errors.key?(:field_2) || errors.key?(:field_3) + + unless bulk_upload.form.valid_start_date_for_form?(saledate) + errors.add(:field_1, I18n.t("#{ERROR_BASE_KEY}.saledate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + errors.add(:field_2, I18n.t("#{ERROR_BASE_KEY}.saledate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + errors.add(:field_3, I18n.t("#{ERROR_BASE_KEY}.saledate.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + end + end + + def validate_if_log_already_exists + if log_already_exists? + error_message = I18n.t("#{ERROR_BASE_KEY}.duplicate") + + errors.add(:field_4, error_message) # Owning org + errors.add(:field_1, error_message) # Sale completion date + errors.add(:field_2, error_message) # Sale completion date + errors.add(:field_3, error_message) # Sale completion date + errors.add(:field_21, error_message) # Postcode + errors.add(:field_22, error_message) # Postcode + errors.add(:field_28, error_message) # Buyer 1 age + errors.add(:field_29, error_message) # Buyer 1 gender + errors.add(:field_32, error_message) # Buyer 1 working situation + errors.add(:field_7, error_message) # Purchaser code + end + end + + def validate_incomplete_soft_validations + routed_to_soft_validation_questions = log.form.questions.filter { |q| q.type == "interruption_screen" && q.page.routed_to?(log, nil) }.compact + routed_to_soft_validation_questions.each do |question| + next if question.completed?(log) + + question.page.interruption_screen_question_ids.each do |interruption_screen_question_id| + next if log.form.questions.none? { |q| q.id == interruption_screen_question_id && q.page.routed_to?(log, nil) } + + field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field| + if errors.none? { |e| e.options[:category] == :soft_validation && field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) } + error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(" ") + errors.add(field, message: error_message, category: :soft_validation) + end + end + end + end + end + + def validate_buyer1_economic_status + if field_32 == 9 + if field_28.present? && field_28.to_i >= 16 + errors.add(:field_32, I18n.t("#{ERROR_BASE_KEY}.ecstat1.buyer_cannot_be_over_16_and_child")) + errors.add(:field_28, I18n.t("#{ERROR_BASE_KEY}.age1.buyer_cannot_be_over_16_and_child")) + else + errors.add(:field_32, I18n.t("#{ERROR_BASE_KEY}.ecstat1.buyer_cannot_be_child")) + end + end + end + + def validate_buyer2_economic_status + return unless joint_purchase? + + if field_39 == 9 + if field_35.present? && field_35.to_i >= 16 + errors.add(:field_39, I18n.t("#{ERROR_BASE_KEY}.ecstat2.buyer_cannot_be_over_16_and_child")) + errors.add(:field_35, I18n.t("#{ERROR_BASE_KEY}.age2.buyer_cannot_be_over_16_and_child")) + else + errors.add(:field_39, I18n.t("#{ERROR_BASE_KEY}.ecstat2.buyer_cannot_be_child")) + end + end + end + + def validate_nationality + if field_31.present? && !valid_nationality_options.include?(field_31.to_s) + errors.add(:field_31, I18n.t("#{ERROR_BASE_KEY}.nationality.invalid")) + end + end + + def validate_buyer_2_nationality + if field_38.present? && !valid_nationality_options.include?(field_38.to_s) + errors.add(:field_38, I18n.t("#{ERROR_BASE_KEY}.nationality.invalid")) + end + end + + def valid_nationality_options + %w[0] + GlobalConstants::COUNTRIES_ANSWER_OPTIONS.keys # 0 is "Prefers not to say" + end + + def bulk_upload_organisation + Organisation.find(bulk_upload.organisation_id) + end +end diff --git a/app/services/csv/lettings_log_csv_service.rb b/app/services/csv/lettings_log_csv_service.rb index 3c4324d14..0a187d524 100644 --- a/app/services/csv/lettings_log_csv_service.rb +++ b/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 diff --git a/app/services/csv/sales_log_csv_service.rb b/app/services/csv/sales_log_csv_service.rb index f74684868..08ce178e3 100644 --- a/app/services/csv/sales_log_csv_service.rb +++ b/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) diff --git a/app/services/uprn_data_presenter.rb b/app/services/uprn_data_presenter.rb index 7c70a81e3..049afb1b4 100644 --- a/app/services/uprn_data_presenter.rb +++ b/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 diff --git a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb similarity index 88% rename from app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb rename to app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb index ce873b6d7..887b81554 100644 --- a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb +++ b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file.html.erb @@ -14,7 +14,7 @@

Download template

-

<%= govuk_link_to "Download the lettings bulk upload template (2024 to 2025)", @form.template_path %>

+

<%= govuk_link_to "Download the lettings bulk upload template (#{@form.year_combo})", @form.template_path %>

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.

Create your file

@@ -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, "Username field: 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 %> diff --git a/app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb b/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb similarity index 82% rename from app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb rename to app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb index 723ae5314..00c693cd4 100644 --- a/app/views/bulk_upload_sales_logs/forms/prepare_your_file_2024.html.erb +++ b/app/views/bulk_upload_sales_logs/forms/prepare_your_file.html.erb @@ -14,15 +14,15 @@

Download template

-

Use one of these templates to upload logs for 2024/25:

-

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

+

Use one of these templates to upload logs for <%= @form.slash_year_combo %>:

+

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

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.

Create your file

<%= 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, "Username field: 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 %> diff --git a/app/views/form/_address_search_question.html.erb b/app/views/form/_address_search_question.html.erb new file mode 100644 index 000000000..ea30be718 --- /dev/null +++ b/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| %> + + <% end %> + <% else %> + + <% end %> +<% end %> + +<%= render partial: "form/guidance/#{question.bottom_guidance_partial}" if question.bottom_guidance? %> diff --git a/app/views/form/_select_question.html.erb b/app/views/form/_select_question.html.erb index 2dceffd63..7a3b19c32 100644 --- a/app/views/form/_select_question.html.erb +++ b/app/views/form/_select_question.html.erb @@ -1,20 +1,23 @@ - <% 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": "accessible-autocomplete", - caption: caption(caption_text, page_header, conditional), - hint: { text: question.hint_text&.html_safe }) do %> + label: legend(question, page_header, conditional), + "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| %> + 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 %> <% end %> + <% else %> + <% end %> +<% end %> <%= render partial: "form/guidance/#{question.bottom_guidance_partial}" if question.bottom_guidance? %> diff --git a/app/views/form/guidance/_address_fallback.html.erb b/app/views/form/guidance/_address_fallback.html.erb new file mode 100644 index 000000000..4ee0bd608 --- /dev/null +++ b/app/views/form/guidance/_address_fallback.html.erb @@ -0,0 +1,3 @@ +
+ <%= govuk_link_to "Clear address and search instead", address_search_input_path(@log.log_type, @log.id), class: "govuk-button govuk-button--secondary" %> +
diff --git a/app/views/form/guidance/_address_search.html.erb b/app/views/form/guidance/_address_search.html.erb new file mode 100644 index 000000000..e3ac31657 --- /dev/null +++ b/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 %> + +
+ <%= govuk_link_to "Enter the address manually instead", address_manual_input_path(@log.log_type, @log.id), class: "govuk-button govuk-button--secondary" %> +
diff --git a/app/views/logs/edit.html.erb b/app/views/logs/edit.html.erb index 3859ca6db..91d249362 100644 --- a/app/views/logs/edit.html.erb +++ b/app/views/logs/edit.html.erb @@ -25,6 +25,7 @@ <% end %>

<% elsif @log.status == "not_started" %> +

<%= govuk_link_to "Guidance for submitting social housing lettings and sales data (opens in a new tab)", guidance_path, target: "#" %>

This log has not been started.

<% elsif @log.status == "completed" %>

@@ -36,6 +37,7 @@

<% end %> + <%= deadline_text(@log) %> <%= render "tasklist" %> <%= edit_actions_for_log(@log, bulk_upload_filter_applied) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 6ca3ea322..3e35a32ce 100644 --- a/config/locales/en.yml +++ b/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: diff --git a/config/locales/forms/2024/lettings/guidance.en.yml b/config/locales/forms/2024/lettings/guidance.en.yml index 922124a94..ba5d152d6 100644 --- a/config/locales/forms/2024/lettings/guidance.en.yml +++ b/config/locales/forms/2024/lettings/guidance.en.yml @@ -14,7 +14,7 @@ en:

If your organisation’s schemes were migrated from old CORE, they may have new names and codes. Search by postcode to find your scheme.

" scheme_changes_link_text: "Read more about how schemes have changed" view_schemes_link_text: "View your organisation’s schemes" - + privacy_notice_tenant: content: "Make sure the lead tenant 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" @@ -60,4 +60,11 @@ en:
  • housing benefit
  • child benefit
  • council tax support
  • - " \ No newline at end of file + " + + address_search: + title: "Can’t find the address you’re looking for?" + content: "" diff --git a/config/locales/forms/2024/lettings/property_information.en.yml b/config/locales/forms/2024/lettings/property_information.en.yml index 9e7326040..016c78958 100644 --- a/config/locales/forms/2024/lettings/property_information.en.yml +++ b/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: diff --git a/config/locales/forms/2024/sales/guidance.en.yml b/config/locales/forms/2024/sales/guidance.en.yml index 801c43a5c..b57595c66 100644 --- a/config/locales/forms/2024/sales/guidance.en.yml +++ b/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: "" diff --git a/config/locales/forms/2024/sales/property_information.en.yml b/config/locales/forms/2024/sales/property_information.en.yml index b40e40267..518654211 100644 --- a/config/locales/forms/2024/sales/property_information.en.yml +++ b/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: diff --git a/config/locales/forms/2025/lettings/guidance.en.yml b/config/locales/forms/2025/lettings/guidance.en.yml index 340eb11d5..cb51fbf9d 100644 --- a/config/locales/forms/2025/lettings/guidance.en.yml +++ b/config/locales/forms/2025/lettings/guidance.en.yml @@ -61,3 +61,10 @@ en:
  • child benefit
  • council tax support
  • " + + address_search: + title: "Can’t find the address you’re looking for?" + content: "" diff --git a/config/locales/forms/2025/lettings/property_information.en.yml b/config/locales/forms/2025/lettings/property_information.en.yml index 22a0d12db..41cf2d3aa 100644 --- a/config/locales/forms/2025/lettings/property_information.en.yml +++ b/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.

    The UPRN may not be the same as the property reference assigned by your organisation.

    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?" diff --git a/config/locales/forms/2025/sales/guidance.en.yml b/config/locales/forms/2025/sales/guidance.en.yml index 4ed6796b3..cfb9b0615 100644 --- a/config/locales/forms/2025/sales/guidance.en.yml +++ b/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: "" diff --git a/config/locales/forms/2025/sales/property_information.en.yml b/config/locales/forms/2025/sales/property_information.en.yml index 332219a6b..e91089bc5 100644 --- a/config/locales/forms/2025/sales/property_information.en.yml +++ b/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.

    The UPRN may not be the same as the property reference assigned by your organisation.

    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?" diff --git a/config/locales/forms/2025/sales/sale_information.en.yml b/config/locales/forms/2025/sales/sale_information.en.yml index 93f5226b5..95eed6185 100644 --- a/config/locales/forms/2025/sales/sale_information.en.yml +++ b/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" diff --git a/config/locales/validations/lettings/2025/bulk_upload.en.yml b/config/locales/validations/lettings/2025/bulk_upload.en.yml new file mode 100644 index 000000000..7acb0726c --- /dev/null +++ b/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'." diff --git a/config/locales/validations/sales/2025/bulk_upload.en.yml b/config/locales/validations/sales/2025/bulk_upload.en.yml new file mode 100644 index 000000000..c9d194fd0 --- /dev/null +++ b/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." diff --git a/config/locales/validations/sales/property_information.en.yml b/config/locales/validations/sales/property_information.en.yml index 9014f1d78..f421177af 100644 --- a/config/locales/validations/sales/property_information.en.yml +++ b/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." diff --git a/config/routes.rb b/config/routes.rb index ab31b6b1b..304d54ef0 100644 --- a/config/routes.rb +++ b/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 diff --git a/db/migrate/20250219122817_add_manual_address_entry_selected_to_logs.rb b/db/migrate/20250219122817_add_manual_address_entry_selected_to_logs.rb new file mode 100644 index 000000000..a0d0f7529 --- /dev/null +++ b/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 diff --git a/db/schema.rb b/db/schema.rb index dfacf3d98..82d67b432 100644 --- a/db/schema.rb +++ b/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" diff --git a/lib/tasks/correct_reasonpref_values.rake b/lib/tasks/correct_reasonpref_values.rake new file mode 100644 index 000000000..4ec1bb8f0 --- /dev/null +++ b/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 diff --git a/session-manager-plugin.deb b/session-manager-plugin.deb new file mode 100644 index 000000000..3befd8f9c Binary files /dev/null and b/session-manager-plugin.deb differ diff --git a/spec/factories/lettings_log.rb b/spec/factories/lettings_log.rb index 5d13cacfa..ad81bca5d 100644 --- a/spec/factories/lettings_log.rb +++ b/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 diff --git a/spec/factories/sales_log.rb b/spec/factories/sales_log.rb index 820c99fdc..64137704c 100644 --- a/spec/factories/sales_log.rb +++ b/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" diff --git a/spec/features/form/address_search_spec.rb b/spec/features/form/address_search_spec.rb new file mode 100644 index 000000000..cd1738195 --- /dev/null +++ b/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 diff --git a/spec/features/form/form_navigation_spec.rb b/spec/features/form/form_navigation_spec.rb index 6484fe94c..695115a78 100644 --- a/spec/features/form/form_navigation_spec.rb +++ b/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 diff --git a/spec/features/lettings_log_spec.rb b/spec/features/lettings_log_spec.rb index a400ef2d0..15d24e5ed 100644 --- a/spec/features/lettings_log_spec.rb +++ b/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 diff --git a/spec/features/sales_log_spec.rb b/spec/features/sales_log_spec.rb index 3030d6d6a..3fa89a504 100644 --- a/spec/features/sales_log_spec.rb +++ b/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 diff --git a/spec/fixtures/files/sales_logs_csv_export_codes_24.csv b/spec/fixtures/files/sales_logs_csv_export_codes_24.csv index e0ebccb38..f874e4e75 100644 --- a/spec/fixtures/files/sales_logs_csv_export_codes_24.csv +++ b/spec/fixtures/files/sales_logs_csv_export_codes_24.csv @@ -1,3 +1,3 @@ Log ID,Status of log,ID of a set of duplicate logs,Time and date the log was created,Time and date the log was last updated,Year collection period opened,Was the log submitted in-service or via bulk upload?,ID of a set of bulk uploaded logs,Is the user in the created_by column the data protection officer?,Day of sale completion date,Month of sale completion date,Year of sale completion date,Which organisation owned this property before the sale?,Which organisation reported the sale?,User that created the log,User the log is assigned to,What is the purchaser code?,Was this purchase made through an ownership scheme?,What is the type of shared ownership/discounted ownership/outright sale?,"If type = 'Other', what is the type of outright sale?",Is the buyer a company?,Will the buyer(s) live in the property?,Is this a joint purchase?,Are there more than 2 joint buyers of this property?,Did you interview the buyer to answer these questions?,Has the buyer seen the MHCLG privacy notice?,What is the UPRN of the property?,Address line 1,Address line 2,Town/City,County,Postcode,The internal value to indicate if the LA was inferred from the postcode,LA name,LA code,UPRN of the address selected,Was the 'No address found' page seen?,Address line 1 input from address matching feature,Postcode input from address matching feature,Address line 1 entered in bulk upload file,Address line 2 entered in bulk upload file,Town or city entered in bulk upload file,County entered in bulk upload file,Postcode entered in bulk upload file,Local authority entered in bulk upload file,How many bedrooms does the property have?,What type of unit is the property?,Which type of building is the property?,Is the property built or adapted to wheelchair-user standards?,What is buyer 1's age?,Which of these best describes buyer 1's gender identity?,What is buyer 1's ethnic group?,Which of the following best describes buyer 1's ethnic background?,What is buyer 1's nationality?,Which of these best describes buyer 1's working situation?,Will buyer 1 live in the property?,What is buyer 2 or person 2's relationship to buyer 1?,What is buyer 2 or person 2's age?,Which of these best describes buyer 2 or person 2's gender identity?,What is buyer 2's ethnic group?,Which of the following best describes buyer 2's ethnic background?,What is buyer 2's nationality?,What is buyer 2 or person 2's working situation?,Will buyer 2 live in the property?,"Besides the buyer(s), how many other people live or will live in the property?",What is person 3's relationship to buyer 1?,What is person 3's age?,What is person 3's gender identity?,What is person 3's working situation?,What is person 4's relationship to buyer 1?,What is person 4's age?,What is person 4's gender identity?,What is person 4's working situation?,What is person 5's relationship to buyer 1?,What is person 5's age?,What is person 5's gender identity?,What is person 5's working situation?,What is person 6's relationship to buyer 1?,What is person 6's age?,What is person 6's gender identity?,What is person 6's working situation?,What was buyer 1's previous tenure?,Do you know the postcode of buyer 1's last settled accommodation?,Part 1 of postcode of buyer 1's last settled accommodation,Part 2 of postcode of buyer 1's last settled accommodation,Do you know the local authority of buyer 1's last settled accommodation?,The local authority code of buyer 1's last settled accommodation,The local authority name of buyer 1's last settled accommodation,Was the buyer registered with their PRP (HA)?,Was the buyer registered with another PRP (HA)?,Was the buyer registered with the local authority?,Was the buyer registered with a Help to Buy agent?,"Populated if pregyrha, pregother, pregla and pregghb are blank","At the time of purchase, was buyer 2 living at the same address as buyer 1?",What was buyer 2's previous tenure?,Have any of the buyers ever served as a regular in the UK armed forces?,Is the buyer still serving in the UK armed forces?,Are any of the buyers a spouse or civil partner of a UK armed forces regular who died in service within the last 2 years?,Does anyone in the household consider themselves to have a disability?,Does anyone in the household use a wheelchair?,Is buyer 1's annual income known?,What is buyer 1's annual income?,Was buyer 1's income used for a mortgage application?,Is buyer 1's annual income known?,What is buyer 2's annual income?,Was buyer 2's income used for a mortgage application?,Were the buyers receiving any of these housing-related benefits immediately before buying this property?,Is the the total amount the buyers had in savings known?,What is the total amount the buyers had in savings before they paid any deposit for the property?,Have any of the buyers previously owned a property?,Was the previous property under shared ownership?,How long did the buyer(s) live in the property before purchasing it?,Is this a staircasing transaction?,What percentage of the property has been bought in this staircasing transaction?,What percentage of the property do the buyers now own in total?,Was this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?,Is this a resale?,Day of the exchange of contracts,Month of the exchange of contracts,Year of the exchange of contracts,Day of the practical completion or handover date,Month of the practical completion or handover date,Year of the practical completion or handover date,Was the household rehoused under a local authority nominations agreement?,"Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?",How many bedrooms did the buyer's previous property have?,What was the previous property type?,What was the rent type of buyer's previous tenure?,What is the full purchase price?,Populated if a soft validation is confirmed.,What was the initial percentage equity stake purchased?,Was a mortgage used to buy this property?,What is the mortgage amount?,What is the name of the mortgage lender?,"If mortgagelender = 'Other', what is the name of the mortgage lender?",What is the length of the mortgage in years?,Does this include any extra borrowing?,How much was the cash deposit paid on the property?,How much cash discount was given through Social Homebuy?,What is the basic monthly rent?,Does the property have any monthly leasehold charges?,What are the total monthly leasehold charges for the property?,Populated if a soft validation is confirmed.,What was the percentage discount?,"What was the amount of any loan, grant, discount or subsidy given?" ID,STATUS,DUPLICATESET,CREATEDDATE,UPLOADDATE,COLLECTIONYEAR,CREATIONMETHOD,BULKUPLOADID,DATAPROTECT,DAY,MONTH,YEAR,OWNINGORGNAME,MANINGORGNAME,CREATEDBY,USERNAME,PURCHID,OWNERSHIP,TYPE,OTHTYPE,COMPANY,LIVEINBUYER,JOINT,JOINTMORE,NOINT,PRIVACYNOTICE,UPRN,ADDRESS1,ADDRESS2,TOWNCITY,COUNTY,POSTCODE,ISLAINFERRED,LANAME,LA,UPRNSELECTED,ADDRESS_SEARCH_VALUE_CHECK,ADDRESS1INPUT,POSTCODEINPUT,BULKADDRESS1,BULKADDRESS2,BULKTOWNCITY,BULKCOUNTY,BULKPOSTCODE,BULKLA,BEDS,PROPTYPE,BUILTYPE,WCHAIR,AGE1,SEX1,ETHNICGROUP1,ETHNIC,NATIONALITYALL1,ECSTAT1,LIVEINBUYER1,RELAT2,AGE2,SEX2,ETHNICGROUP2,ETHNIC2,NATIONALITYALL2,ECSTAT2,LIVEINBUYER2,HHTYPE,RELAT3,AGE3,SEX3,ECSTAT3,RELAT4,AGE4,SEX4,ECSTAT4,RELAT5,AGE5,SEX5,ECSTAT5,RELAT6,AGE6,SEX6,ECSTAT6,PREVTEN,PPCODENK,PPOSTC1,PPOSTC2,PREVIOUSLAKNOWN,PREVLOC,PREVLOCNAME,PREGYRHA,PREGOTHER,PREGLA,PREGGHB,PREGBLANK,BUY2LIVING,PREVTEN2,HHREGRES,HHREGRESSTILL,ARMEDFORCESSPOUSE,DISABLED,WHEEL,INC1NK,INCOME1,INC1MORT,INC2NK,INCOME2,INC2MORT,HB,SAVINGSNK,SAVINGS,PREVOWN,PREVSHARED,PROPLEN,STAIRCASE,STAIRBOUGHT,STAIROWNED,STAIRCASETOSALE,RESALE,EXDAY,EXMONTH,EXYEAR,HODAY,HOMONTH,HOYEAR,LANOMAGR,SOCTEN,FROMBEDS,FROMPROP,SOCPREVTEN,VALUE,VALUE_VALUE_CHECK,EQUITY,MORTGAGEUSED,MORTGAGE,MORTGAGELENDER,MORTGAGELENDEROTHER,MORTLEN1,EXTRABOR,DEPOSIT,CASHDIS,MRENT,HASMSCHARGE,MSCHARGE,MSCHARGE_VALUE_CHECK,DISCOUNT,GRANT -,completed,,2024-05-01T00:00:00+01:00,2024-05-01T00:00:00+01:00,2024,1,,false,1,5,2024,MHCLG,MHCLG,billyboy@eyeklaud.com,billyboy@eyeklaud.com,,2,8,,,,1,1,2,1,1,"1, Test Street",,Test Town,,AA1 1AA,true,Westminster,E09000033,,,Address line 1,SW1A 1AA,address line 1 as entered,address line 2 as entered,town or city as entered,county as entered,AB1 2CD,la as entered,2,1,1,1,30,X,17,17,826,1,1,P,35,X,17,,826,1,1,3,C,14,X,9,X,-9,X,3,R,-9,R,10,,,,,1,0,SW1A,1AA,1,E09000033,Westminster,1,1,1,1,,3,,1,4,5,1,1,0,13400,1,0,13400,1,4,1,,1,2,10,,,,,,,,,,,,,,,,,110000.0,,,1,20000.0,5,,10,1,80000.0,,,1,100.0,,,10000.0 +,completed,,2024-05-01T00:00:00+01:00,2024-05-01T00:00:00+01:00,2024,1,,false,1,5,2024,MHCLG,MHCLG,billyboy@eyeklaud.com,billyboy@eyeklaud.com,,2,8,,,,1,1,2,1,1,"1, Test Street",,Test Town,,AA1 1AA,true,Westminster,E09000033,1,,,,address line 1 as entered,address line 2 as entered,town or city as entered,county as entered,AB1 2CD,la as entered,2,1,1,1,30,X,17,17,826,1,1,P,35,X,17,,826,1,1,3,C,14,X,9,X,-9,X,3,R,-9,R,10,,,,,1,0,SW1A,1AA,1,E09000033,Westminster,1,1,1,1,,3,,1,4,5,1,1,0,13400,1,0,13400,1,4,1,,1,2,10,,,,,,,,,,,,,,,,,110000.0,,,1,20000.0,5,,10,1,80000.0,,,1,100.0,,,10000.0 diff --git a/spec/fixtures/files/sales_logs_csv_export_labels_24.csv b/spec/fixtures/files/sales_logs_csv_export_labels_24.csv index ffda37642..a66a16201 100644 --- a/spec/fixtures/files/sales_logs_csv_export_labels_24.csv +++ b/spec/fixtures/files/sales_logs_csv_export_labels_24.csv @@ -1,3 +1,3 @@ Log ID,Status of log,ID of a set of duplicate logs,Time and date the log was created,Time and date the log was last updated,Year collection period opened,Was the log submitted in-service or via bulk upload?,ID of a set of bulk uploaded logs,Is the user in the created_by column the data protection officer?,Day of sale completion date,Month of sale completion date,Year of sale completion date,Which organisation owned this property before the sale?,Which organisation reported the sale?,User that created the log,User the log is assigned to,What is the purchaser code?,Was this purchase made through an ownership scheme?,What is the type of shared ownership/discounted ownership/outright sale?,"If type = 'Other', what is the type of outright sale?",Is the buyer a company?,Will the buyer(s) live in the property?,Is this a joint purchase?,Are there more than 2 joint buyers of this property?,Did you interview the buyer to answer these questions?,Has the buyer seen the MHCLG privacy notice?,"What is the UPRN of the property?",Address line 1,Address line 2,Town/City,County,Postcode,The internal value to indicate if the LA was inferred from the postcode,LA name,LA code,UPRN of the address selected,Was the 'No address found' page seen?,Address line 1 input from address matching feature,Postcode input from address matching feature,Address line 1 entered in bulk upload file,Address line 2 entered in bulk upload file,Town or city entered in bulk upload file,County entered in bulk upload file,Postcode entered in bulk upload file,Local authority entered in bulk upload file,How many bedrooms does the property have?,What type of unit is the property?,Which type of building is the property?,Is the property built or adapted to wheelchair-user standards?,What is buyer 1's age?,Which of these best describes buyer 1's gender identity?,What is buyer 1's ethnic group?,Which of the following best describes buyer 1's ethnic background?,What is buyer 1's nationality?,Which of these best describes buyer 1's working situation?,Will buyer 1 live in the property?,What is buyer 2 or person 2's relationship to buyer 1?,What is buyer 2 or person 2's age?,Which of these best describes buyer 2 or person 2's gender identity?,What is buyer 2's ethnic group?,Which of the following best describes buyer 2's ethnic background?,What is buyer 2's nationality?,What is buyer 2 or person 2's working situation?,Will buyer 2 live in the property?,"Besides the buyer(s), how many other people live or will live in the property?",What is person 3's relationship to buyer 1?,What is person 3's age?,What is person 3's gender identity?,What is person 3's working situation?,What is person 4's relationship to buyer 1?,What is person 4's age?,What is person 4's gender identity?,What is person 4's working situation?,What is person 5's relationship to buyer 1?,What is person 5's age?,What is person 5's gender identity?,What is person 5's working situation?,What is person 6's relationship to buyer 1?,What is person 6's age?,What is person 6's gender identity?,What is person 6's working situation?,What was buyer 1's previous tenure?,Do you know the postcode of buyer 1's last settled accommodation?,Part 1 of postcode of buyer 1's last settled accommodation,Part 2 of postcode of buyer 1's last settled accommodation,Do you know the local authority of buyer 1's last settled accommodation?,The local authority code of buyer 1's last settled accommodation,The local authority name of buyer 1's last settled accommodation,Was the buyer registered with their PRP (HA)?,Was the buyer registered with another PRP (HA)?,Was the buyer registered with the local authority?,Was the buyer registered with a Help to Buy agent?,"Populated if pregyrha, pregother, pregla and pregghb are blank","At the time of purchase, was buyer 2 living at the same address as buyer 1?",What was buyer 2's previous tenure?,Have any of the buyers ever served as a regular in the UK armed forces?,Is the buyer still serving in the UK armed forces?,Are any of the buyers a spouse or civil partner of a UK armed forces regular who died in service within the last 2 years?,Does anyone in the household consider themselves to have a disability?,Does anyone in the household use a wheelchair?,Is buyer 1's annual income known?,What is buyer 1's annual income?,Was buyer 1's income used for a mortgage application?,Is buyer 1's annual income known?,What is buyer 2's annual income?,Was buyer 2's income used for a mortgage application?,Were the buyers receiving any of these housing-related benefits immediately before buying this property?,Is the the total amount the buyers had in savings known?,What is the total amount the buyers had in savings before they paid any deposit for the property?,Have any of the buyers previously owned a property?,Was the previous property under shared ownership?,How long did the buyer(s) live in the property before purchasing it?,Is this a staircasing transaction?,What percentage of the property has been bought in this staircasing transaction?,What percentage of the property do the buyers now own in total?,Was this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?,Is this a resale?,Day of the exchange of contracts,Month of the exchange of contracts,Year of the exchange of contracts,Day of the practical completion or handover date,Month of the practical completion or handover date,Year of the practical completion or handover date,Was the household rehoused under a local authority nominations agreement?,"Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?",How many bedrooms did the buyer's previous property have?,What was the previous property type?,What was the rent type of buyer's previous tenure?,What is the full purchase price?,Populated if a soft validation is confirmed.,What was the initial percentage equity stake purchased?,Was a mortgage used to buy this property?,What is the mortgage amount?,What is the name of the mortgage lender?,"If mortgagelender = 'Other', what is the name of the mortgage lender?",What is the length of the mortgage in years?,Does this include any extra borrowing?,How much was the cash deposit paid on the property?,How much cash discount was given through Social Homebuy?,What is the basic monthly rent?,Does the property have any monthly leasehold charges?,What are the total monthly leasehold charges for the property?,Populated if a soft validation is confirmed.,What was the percentage discount?,"What was the amount of any loan, grant, discount or subsidy given?" ID,STATUS,DUPLICATESET,CREATEDDATE,UPLOADDATE,COLLECTIONYEAR,CREATIONMETHOD,BULKUPLOADID,DATAPROTECT,DAY,MONTH,YEAR,OWNINGORGNAME,MANINGORGNAME,CREATEDBY,USERNAME,PURCHID,OWNERSHIP,TYPE,OTHTYPE,COMPANY,LIVEINBUYER,JOINT,JOINTMORE,NOINT,PRIVACYNOTICE,UPRN,ADDRESS1,ADDRESS2,TOWNCITY,COUNTY,POSTCODE,ISLAINFERRED,LANAME,LA,UPRNSELECTED,ADDRESS_SEARCH_VALUE_CHECK,ADDRESS1INPUT,POSTCODEINPUT,BULKADDRESS1,BULKADDRESS2,BULKTOWNCITY,BULKCOUNTY,BULKPOSTCODE,BULKLA,BEDS,PROPTYPE,BUILTYPE,WCHAIR,AGE1,SEX1,ETHNICGROUP1,ETHNIC,NATIONALITYALL1,ECSTAT1,LIVEINBUYER1,RELAT2,AGE2,SEX2,ETHNICGROUP2,ETHNIC2,NATIONALITYALL2,ECSTAT2,LIVEINBUYER2,HHTYPE,RELAT3,AGE3,SEX3,ECSTAT3,RELAT4,AGE4,SEX4,ECSTAT4,RELAT5,AGE5,SEX5,ECSTAT5,RELAT6,AGE6,SEX6,ECSTAT6,PREVTEN,PPCODENK,PPOSTC1,PPOSTC2,PREVIOUSLAKNOWN,PREVLOC,PREVLOCNAME,PREGYRHA,PREGOTHER,PREGLA,PREGGHB,PREGBLANK,BUY2LIVING,PREVTEN2,HHREGRES,HHREGRESSTILL,ARMEDFORCESSPOUSE,DISABLED,WHEEL,INC1NK,INCOME1,INC1MORT,INC2NK,INCOME2,INC2MORT,HB,SAVINGSNK,SAVINGS,PREVOWN,PREVSHARED,PROPLEN,STAIRCASE,STAIRBOUGHT,STAIROWNED,STAIRCASETOSALE,RESALE,EXDAY,EXMONTH,EXYEAR,HODAY,HOMONTH,HOYEAR,LANOMAGR,SOCTEN,FROMBEDS,FROMPROP,SOCPREVTEN,VALUE,VALUE_VALUE_CHECK,EQUITY,MORTGAGEUSED,MORTGAGE,MORTGAGELENDER,MORTGAGELENDEROTHER,MORTLEN1,EXTRABOR,DEPOSIT,CASHDIS,MRENT,HASMSCHARGE,MSCHARGE,MSCHARGE_VALUE_CHECK,DISCOUNT,GRANT -,completed,,2024-05-01T00:00:00+01:00,2024-05-01T00:00:00+01:00,2024,single log,,false,1,5,2024,MHCLG,MHCLG,billyboy@eyeklaud.com,billyboy@eyeklaud.com,,Yes - a discounted ownership scheme,Right to Acquire (RTA),,,,Yes,Yes,Yes,1,1,"1, Test Street",,Test Town,,AA1 1AA,Yes,Westminster,E09000033,,,Address line 1,SW1A 1AA,address line 1 as entered,address line 2 as entered,town or city as entered,county as entered,AB1 2CD,la as entered,2,Flat or maisonette,Purpose built,Yes,30,Non-binary,Buyer prefers not to say,17,United Kingdom,Full-time - 30 hours or more,Yes,Partner,35,Non-binary,Buyer prefers not to say,,United Kingdom,Full-time - 30 hours or more,Yes,3,Child,14,Non-binary,Child under 16,Other,Not known,Non-binary,In government training into work,Prefers not to say,Not known,Prefers not to say,Prefers not to say,,,,,Local authority tenant,Yes,AA1,1AA,Yes,E09000033,Westminster,1,1,1,1,,Don't know,,Yes,Yes,No,Yes,Yes,Yes,13400,Yes,Yes,13400,Yes,Don’t know ,No,,Yes,No,10,,,,,,,,,,,,,,,,,110000.0,,,Yes,20000.0,Cambridge Building Society,,10,Yes,80000.0,,,Yes,100.0,,,10000.0 +,completed,,2024-05-01T00:00:00+01:00,2024-05-01T00:00:00+01:00,2024,single log,,false,1,5,2024,MHCLG,MHCLG,billyboy@eyeklaud.com,billyboy@eyeklaud.com,,Yes - a discounted ownership scheme,Right to Acquire (RTA),,,,Yes,Yes,Yes,1,1,"1, Test Street",,Test Town,,AA1 1AA,Yes,Westminster,E09000033,1,,,,address line 1 as entered,address line 2 as entered,town or city as entered,county as entered,AB1 2CD,la as entered,2,Flat or maisonette,Purpose built,Yes,30,Non-binary,Buyer prefers not to say,17,United Kingdom,Full-time - 30 hours or more,Yes,Partner,35,Non-binary,Buyer prefers not to say,,United Kingdom,Full-time - 30 hours or more,Yes,3,Child,14,Non-binary,Child under 16,Other,Not known,Non-binary,In government training into work,Prefers not to say,Not known,Prefers not to say,Prefers not to say,,,,,Local authority tenant,Yes,SW1A,1AA,Yes,E09000033,Westminster,1,1,1,1,,Don't know,,Yes,Yes,No,Yes,Yes,Yes,13400,Yes,Yes,13400,Yes,Don’t know ,No,,Yes,No,10,,,,,,,,,,,,,,,,,110000.0,,,Yes,20000.0,Cambridge Building Society,,10,Yes,80000.0,,,Yes,100.0,,,10000.0 diff --git a/spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv b/spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv index 0e9a781f5..a6eaed17a 100644 --- a/spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv +++ b/spec/fixtures/files/sales_logs_csv_export_non_support_labels_24.csv @@ -1,3 +1,3 @@ Log ID,Status of log,ID of a set of duplicate logs,Time and date the log was created,Time and date the log was last updated,Year collection period opened,Was the log submitted in-service or via bulk upload?,,Is the user in the assigned_to column the data protection officer?,Day of sale completion date,Month of sale completion date,Year of sale completion date,Which organisation owned this property before the sale?,Which organisation reported the sale?,User the log is assigned to,What is the purchaser code?,Was this purchase made through an ownership scheme?,What is the type of shared ownership/discounted ownership/outright sale?,"If type = 'Other', what is the type of outright sale?",Is the buyer a company?,Will the buyer(s) live in the property?,Is this a joint purchase?,Are there more than 2 joint buyers of this property?,Did you interview the buyer to answer these questions?,Has the buyer seen the MHCLG privacy notice?,What is the UPRN of the property?,We found an address that might be this property. Is this the property address?,Address line 1 input from address matching feature,Postcode input from address matching feature,UPRN of the address selected,Address line 1,Address line 2,Town/City,County,Part 1 of the property's postcode,Part 2 of the property's postcode,LA code,LA name,How many bedrooms does the property have?,What type of unit is the property?,Which type of building is the property?,Is the property built or adapted to wheelchair-user standards?,What is buyer 1's age?,Which of these best describes buyer 1's gender identity?,What is buyer 1's ethnic group?,Which of the following best describes buyer 1's ethnic background?,What is buyer 1's nationality?,Which of these best describes buyer 1's working situation?,Will buyer 1 live in the property?,What is buyer 2 or person 2's relationship to buyer 1?,What is buyer 2 or person 2's age?,Which of these best describes buyer 2 or person 2's gender identity?,What is buyer 2's ethnic group?,Which of the following best describes buyer 2's ethnic background?,What is buyer 2's nationality?,What is buyer 2 or person 2's working situation?,Will buyer 2 live in the property?,"Besides the buyer(s), how many other people live or will live in the property?",What is person 3's relationship to buyer 1?,What is person 3's age?,What is person 3's gender identity?,What is person 3's working situation?,What is person 4's relationship to buyer 1?,What is person 4's age?,What is person 4's gender identity?,What is person 4's working situation?,What is person 5's relationship to buyer 1?,What is person 5's age?,What is person 5's gender identity?,What is person 5's working situation?,What is person 6's relationship to buyer 1?,What is person 6's age?,What is person 6's gender identity?,What is person 6's working situation?,What was buyer 1's previous tenure?,Do you know the postcode of buyer 1's last settled accommodation?,Part 1 of postcode of buyer 1's last settled accommodation,Part 2 of postcode of buyer 1's last settled accommodation,Do you know the local authority of buyer 1's last settled accommodation?,The local authority code of buyer 1's last settled accommodation,The local authority name of buyer 1's last settled accommodation,Was the buyer registered with their PRP (HA)?,Was the buyer registered with another PRP (HA)?,Was the buyer registered with the local authority?,Was the buyer registered with a Help to Buy agent?,"Populated if pregyrha, pregother, pregla and pregghb are blank","At the time of purchase, was buyer 2 living at the same address as buyer 1?",What was buyer 2's previous tenure?,Have any of the buyers ever served as a regular in the UK armed forces?,Is the buyer still serving in the UK armed forces?,Are any of the buyers a spouse or civil partner of a UK armed forces regular who died in service within the last 2 years?,Does anyone in the household consider themselves to have a disability?,Does anyone in the household use a wheelchair?,Is buyer 1's annual income known?,What is buyer 1's annual income?,Was buyer 1's income used for a mortgage application?,Is buyer 2's annual income known?,What is buyer 2's annual income?,Was buyer 2's income used for a mortgage application?,Were the buyers receiving any of these housing-related benefits immediately before buying this property?,Is the the total amount the buyers had in savings known?,What is the total amount the buyers had in savings before they paid any deposit for the property?,Have any of the buyers previously owned a property?,Was the previous property under shared ownership?,How long did the buyer(s) live in the property before purchasing it?,Is this a staircasing transaction?,What percentage of the property has been bought in this staircasing transaction?,What percentage of the property do the buyers now own in total?,Was this transaction part of a back-to-back staircasing transaction to facilitate sale of the home on the open market?,Is this a resale?,Day of the exchange of contracts,Month of the exchange of contracts,Year of the exchange of contracts,Day of the practical completion or handover date,Month of the practical completion or handover date,Year of the practical completion or handover date,Was the household rehoused under a local authority nominations agreement?,"Was the buyer a private registered provider, housing association or local authority tenant immediately before this sale?",How many bedrooms did the buyer's previous property have?,What was the previous property type?,What was the rent type of buyer's previous tenure?,What is the full purchase price?,What was the initial percentage equity stake purchased?,Was a mortgage used to buy this property?,What is the mortgage amount?,What is the name of the mortgage lender?,"If mortgagelender = 'Other', what is the name of the mortgage lender?",What is the length of the mortgage in years?,Does this include any extra borrowing?,How much was the cash deposit paid on the property?,How much cash discount was given through Social Homebuy?,What is the basic monthly rent?,Does the property have any monthly leasehold charges?,What are the total monthly leasehold charges for the property?,What was the percentage discount?,"What was the amount of any loan, grant, discount or subsidy given?" id,status,duplicate_set_id,created_at,updated_at,collection_start_year,creation_method,bulk_upload_id,is_dpo,day,month,year,owning_organisation_name,managing_organisation_name,assigned_to,purchid,ownershipsch,type,othtype,companybuy,buylivein,jointpur,jointmore,noint,privacynotice,uprn,uprn_confirmed,address_line1_input,postcode_full_input,uprn_selection,address_line1,address_line2,town_or_city,county,pcode1,pcode2,la,la_label,beds,proptype,builtype,wchair,age1,sex1,ethnic_group,ethnic,nationality_all,ecstat1,buy1livein,relat2,age2,sex2,ethnic_group2,ethnicbuy2,nationality_all_buyer2,ecstat2,buy2livein,hholdcount,relat3,age3,sex3,ecstat3,relat4,age4,sex4,ecstat4,relat5,age5,sex5,ecstat5,relat6,age6,sex6,ecstat6,prevten,ppcodenk,ppostc1,ppostc2,previous_la_known,prevloc,prevloc_label,pregyrha,pregother,pregla,pregghb,pregblank,buy2living,prevtenbuy2,hhregres,hhregresstill,armedforcesspouse,disabled,wheel,income1nk,income1,inc1mort,income2nk,income2,inc2mort,hb,savingsnk,savings,prevown,prevshared,proplen,staircase,stairbought,stairowned,staircasesale,resale,exday,exmonth,exyear,hoday,homonth,hoyear,lanomagr,soctenant,frombeds,fromprop,socprevten,value,equity,mortgageused,mortgage,mortgagelender,mortgagelenderother,mortlen,extrabor,deposit,cashdis,mrent,has_mscharge,mscharge,discount,grant -,completed,,2024-05-01T00:00:00+01:00,2024-05-01T00:00:00+01:00,2024,single log,,false,1,5,2024,MHCLG,MHCLG,billyboy@eyeklaud.com,,Yes - a discounted ownership scheme,Right to Acquire (RTA),,,,Yes,Yes,Yes,1,1,Yes,Address line 1,SW1A 1AA,,"1, Test Street",,Test Town,,AA1,1AA,E09000033,Westminster,2,Flat or maisonette,Purpose built,Yes,30,Non-binary,Buyer prefers not to say,17,United Kingdom,Full-time - 30 hours or more,Yes,Partner,35,Non-binary,Buyer prefers not to say,,United Kingdom,Full-time - 30 hours or more,Yes,3,Child,14,Non-binary,Child under 16,Other,Not known,Non-binary,In government training into work,Prefers not to say,Not known,Prefers not to say,Prefers not to say,,,,,Local authority tenant,Yes,AA1,1AA,Yes,E09000033,Westminster,1,1,1,1,,Don't know,,Yes,Yes,No,Yes,Yes,Yes,13400,Yes,Yes,13400,Yes,Don’t know ,No,,Yes,No,10,,,,,,,,,,,,,,,,,110000.0,,Yes,20000.0,Cambridge Building Society,,10,Yes,80000.0,,,Yes,100.0,,10000.0 +,completed,,2024-05-01T00:00:00+01:00,2024-05-01T00:00:00+01:00,2024,single log,,false,1,5,2024,MHCLG,MHCLG,billyboy@eyeklaud.com,,Yes - a discounted ownership scheme,Right to Acquire (RTA),,,,Yes,Yes,Yes,1,1,Yes,,,1,"1, Test Street",,Test Town,,SW1A,1AA,E09000033,Westminster,2,Flat or maisonette,Purpose built,Yes,30,Non-binary,Buyer prefers not to say,17,United Kingdom,Full-time - 30 hours or more,Yes,Partner,35,Non-binary,Buyer prefers not to say,,United Kingdom,Full-time - 30 hours or more,Yes,3,Child,14,Non-binary,Child under 16,Other,Not known,Non-binary,In government training into work,Prefers not to say,Not known,Prefers not to say,Prefers not to say,,,,,Local authority tenant,Yes,SW1A,1AA,Yes,E09000033,Westminster,1,1,1,1,,Don't know,,Yes,Yes,No,Yes,Yes,Yes,13400,Yes,Yes,13400,Yes,Don’t know ,No,,Yes,No,10,,,,,,,,,,,,,,,,,110000.0,,Yes,20000.0,Cambridge Building Society,,10,Yes,80000.0,,,Yes,100.0,,10000.0 diff --git a/spec/helpers/tasklist_helper_spec.rb b/spec/helpers/tasklist_helper_spec.rb index c5e1bd784..dc683c7b2 100644 --- a/spec/helpers/tasklist_helper_spec.rb +++ b/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 diff --git a/spec/lib/tasks/correct_reasonpref_values_spec.rb b/spec/lib/tasks/correct_reasonpref_values_spec.rb new file mode 100644 index 000000000..8a805e184 --- /dev/null +++ b/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 diff --git a/spec/models/bulk_upload_spec.rb b/spec/models/bulk_upload_spec.rb index 03342b627..5195d23f4 100644 --- a/spec/models/bulk_upload_spec.rb +++ b/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]) } diff --git a/spec/models/form/lettings/pages/address_fallback_spec.rb b/spec/models/form/lettings/pages/address_fallback_spec.rb index d3971d540..ffac6238e 100644 --- a/spec/models/form/lettings/pages/address_fallback_spec.rb +++ b/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 diff --git a/spec/models/form/lettings/pages/address_search_spec.rb b/spec/models/form/lettings/pages/address_search_spec.rb new file mode 100644 index 000000000..86c6537a1 --- /dev/null +++ b/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 diff --git a/spec/models/form/lettings/questions/address_search_spec.rb b/spec/models/form/lettings/questions/address_search_spec.rb new file mode 100644 index 000000000..d063a94dc --- /dev/null +++ b/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 diff --git a/spec/models/form/lettings/questions/uprn_selection_spec.rb b/spec/models/form/lettings/questions/uprn_selection_spec.rb index c3edc646e..e10b22d89 100644 --- a/spec/models/form/lettings/questions/uprn_selection_spec.rb +++ b/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 diff --git a/spec/models/form/lettings/questions/uprn_spec.rb b/spec/models/form/lettings/questions/uprn_spec.rb index 8bb932b35..0c3ecec85 100644 --- a/spec/models/form/lettings/questions/uprn_spec.rb +++ b/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 diff --git a/spec/models/form/lettings/subsections/property_information_spec.rb b/spec/models/form/lettings/subsections/property_information_spec.rb index 0d3a4e99b..e2b4b701a 100644 --- a/spec/models/form/lettings/subsections/property_information_spec.rb +++ b/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 diff --git a/spec/models/form/sales/pages/address_fallback_spec.rb b/spec/models/form/sales/pages/address_fallback_spec.rb index 4c48a46fe..35bb6cd24 100644 --- a/spec/models/form/sales/pages/address_fallback_spec.rb +++ b/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 diff --git a/spec/models/form/sales/pages/address_search_spec.rb b/spec/models/form/sales/pages/address_search_spec.rb new file mode 100644 index 000000000..670854694 --- /dev/null +++ b/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 diff --git a/spec/models/form/sales/pages/living_before_purchase_spec.rb b/spec/models/form/sales/pages/living_before_purchase_spec.rb index b597f90e9..1f7cf6c51 100644 --- a/spec/models/form/sales/pages/living_before_purchase_spec.rb +++ b/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 diff --git a/spec/models/form/sales/pages/uprn_confirmation_spec.rb b/spec/models/form/sales/pages/uprn_confirmation_spec.rb index 9550b9f49..6415c2eb0 100644 --- a/spec/models/form/sales/pages/uprn_confirmation_spec.rb +++ b/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 diff --git a/spec/models/form/sales/questions/address_search_spec.rb b/spec/models/form/sales/questions/address_search_spec.rb new file mode 100644 index 000000000..bb30cbfa0 --- /dev/null +++ b/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 diff --git a/spec/models/form/sales/questions/uprn_selection_spec.rb b/spec/models/form/sales/questions/uprn_selection_spec.rb index ff1b1a6dd..efcf39c67 100644 --- a/spec/models/form/sales/questions/uprn_selection_spec.rb +++ b/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 diff --git a/spec/models/form/sales/subsections/property_information_spec.rb b/spec/models/form/sales/subsections/property_information_spec.rb index ccfeb8e3a..7585d290a 100644 --- a/spec/models/form/sales/subsections/property_information_spec.rb +++ b/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 diff --git a/spec/models/lettings_log_derived_fields_spec.rb b/spec/models/lettings_log_derived_fields_spec.rb index d315db51e..e4edb194e 100644 --- a/spec/models/lettings_log_derived_fields_spec.rb +++ b/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 diff --git a/spec/models/sales_log_derived_fields_spec.rb b/spec/models/sales_log_derived_fields_spec.rb index 8eda0c36f..7827ce282 100644 --- a/spec/models/sales_log_derived_fields_spec.rb +++ b/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) diff --git a/spec/models/sales_log_spec.rb b/spec/models/sales_log_spec.rb index c36884b8d..442febddb 100644 --- a/spec/models/sales_log_spec.rb +++ b/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 diff --git a/spec/models/validations/sales/property_validations_spec.rb b/spec/models/validations/sales/property_validations_spec.rb index c5af3ae78..62b65ee36 100644 --- a/spec/models/validations/sales/property_validations_spec.rb +++ b/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 diff --git a/spec/request_helper.rb b/spec/request_helper.rb index f1f208ec6..2e0cc3d70 100644 --- a/spec/request_helper.rb +++ b/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") diff --git a/spec/requests/address_search_controller_spec.rb b/spec/requests/address_search_controller_spec.rb new file mode 100644 index 000000000..5c2acd11a --- /dev/null +++ b/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 diff --git a/spec/requests/duplicate_logs_controller_spec.rb b/spec/requests/duplicate_logs_controller_spec.rb index 700964fcf..c2c05e748 100644 --- a/spec/requests/duplicate_logs_controller_spec.rb +++ b/spec/requests/duplicate_logs_controller_spec.rb @@ -77,8 +77,8 @@ RSpec.describe DuplicateLogsController, type: :request do end it "displays check your answers for each log with correct questions where UPRN is given" do - lettings_log.update!(uprn: "123", uprn_known: 1, uprn_confirmed: 1) - duplicate_logs[0].update!(uprn: "123", uprn_known: 1, uprn_confirmed: 1) + lettings_log.update!(uprn: "123", uprn_known: 1, uprn_confirmed: 1, manual_address_entry_selected: false) + duplicate_logs[0].update!(uprn: "123", uprn_known: 1, uprn_confirmed: 1, manual_address_entry_selected: false) get "/lettings-logs/#{lettings_log.id}/duplicate-logs?original_log_id=#{lettings_log.id}" expect(page).to have_content("Q5 - Tenancy start date", count: 3) @@ -186,9 +186,9 @@ RSpec.describe DuplicateLogsController, type: :request do end it "displays check your answers for each log with correct questions when UPRN is given" do - sales_log.update!(uprn: "123", uprn_known: 1) - duplicate_logs[0].update!(uprn: "123", uprn_known: 1) - duplicate_logs[1].update!(uprn: "123", uprn_known: 1) + sales_log.update!(uprn: "123", uprn_known: 1, manual_address_entry_selected: false) + duplicate_logs[0].update!(uprn: "123", uprn_known: 1, manual_address_entry_selected: false) + duplicate_logs[1].update!(uprn: "123", uprn_known: 1, manual_address_entry_selected: false) get "/sales-logs/#{sales_log.id}/duplicate-logs?original_log_id=#{sales_log.id}" expect(page).to have_content("Q1 - Sale completion date", count: 3) diff --git a/spec/requests/lettings_logs_controller_spec.rb b/spec/requests/lettings_logs_controller_spec.rb index 55cf3d573..92683edb3 100644 --- a/spec/requests/lettings_logs_controller_spec.rb +++ b/spec/requests/lettings_logs_controller_spec.rb @@ -1155,6 +1155,21 @@ RSpec.describe LettingsLogsController, type: :request do expect(lettings_log.status).to eq("completed") expect(page).to have_link("review and make changes to this log", href: "/lettings-logs/#{lettings_log.id}/review") end + + it "does not show guidance link" do + expect(page).not_to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)") + end + end + + context "and the log is not started" do + let(:lettings_log) { create(:lettings_log, status: "not_started", assigned_to: user) } + + it "shows guidance link" do + allow(Time.zone).to receive(:now).and_return(lettings_log.form.edit_end_date - 1.day) + get lettings_log_path(lettings_log) + expect(lettings_log.status).to eq("not_started") + expect(page).to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)") + end end context "with bulk_upload_id filter" do diff --git a/spec/requests/sales_logs_controller_spec.rb b/spec/requests/sales_logs_controller_spec.rb index d8209fa1d..e85ecb813 100644 --- a/spec/requests/sales_logs_controller_spec.rb +++ b/spec/requests/sales_logs_controller_spec.rb @@ -866,6 +866,7 @@ RSpec.describe SalesLogsController, type: :request do context "when viewing a sales log" do let(:headers) { { "Accept" => "text/html" } } let(:completed_sales_log) { FactoryBot.create(:sales_log, :completed, owning_organisation: user.organisation, assigned_to: user) } + let(:not_started_sales_log) { FactoryBot.create(:sales_log, owning_organisation: user.organisation, assigned_to: user) } before do sign_in user @@ -956,6 +957,16 @@ RSpec.describe SalesLogsController, type: :request do expect(page).to have_content("This log is from the 2021 to 2022 collection window, which is now closed.") end end + + it "does not show guidance link" do + get "/sales-logs/#{completed_sales_log.id}", headers:, params: {} + expect(page).not_to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)") + end + + it "shows guidance link for not_started log" do + get "/sales-logs/#{not_started_sales_log.id}", headers:, params: {} + expect(page).to have_content("Guidance for submitting social housing lettings and sales data (opens in a new tab)") + end end context "when requesting CSV download" do diff --git a/spec/services/bulk_upload/lettings/validator_spec.rb b/spec/services/bulk_upload/lettings/validator_spec.rb index 60eb8a955..cfe654980 100644 --- a/spec/services/bulk_upload/lettings/validator_spec.rb +++ b/spec/services/bulk_upload/lettings/validator_spec.rb @@ -103,7 +103,7 @@ RSpec.describe BulkUpload::Lettings::Validator do before do values = log_to_csv.to_2024_row values[7] = nil - file.write(log_to_csv.default_2024_field_numbers_row) + file.write(log_to_csv.default_field_numbers_row_for_year(2024)) file.write(log_to_csv.to_custom_csv_row(seed: nil, field_values: values)) file.rewind end @@ -146,7 +146,7 @@ RSpec.describe BulkUpload::Lettings::Validator do before do log.needstype = nil values = log_to_csv.to_2024_row - file.write(log_to_csv.default_2024_field_numbers_row(seed:)) + file.write(log_to_csv.default_field_numbers_row_for_year(2024, seed:)) file.write(log_to_csv.to_custom_csv_row(seed:, field_values: values)) file.close end diff --git a/spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb b/spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb index 43e2f262a..f8cca52ef 100644 --- a/spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2023/csv_parser_spec.rb @@ -15,8 +15,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2023_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023)) file.rewind end @@ -39,8 +39,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2023_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023)) file.rewind end @@ -64,8 +64,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2023_field_numbers_row(seed:)) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row(seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023, seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023, seed:)) file.rewind end @@ -108,7 +108,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do context "when parsing csv without headers" do before do - file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2023_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2023)) file.rewind end @@ -127,7 +127,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do before do file.write(bom) - file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2023_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2023)) file.rewind end @@ -141,7 +141,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do before do file.write(invalid_sequence) - file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2023_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2023)) file.rewind end @@ -158,8 +158,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do file.write("Can be empty?\r") file.write("Type of letting the question applies to\r\n") file.write("Duplicate check field?\r") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2023_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023)) file.rewind end @@ -177,8 +177,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2023_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023)) file.rewind end @@ -190,7 +190,7 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do context "when without headers using default ordering" do before do - file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2023_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2023)) file.rewind end @@ -210,8 +210,8 @@ RSpec.describe BulkUpload::Lettings::Year2023::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2023_field_numbers_row(seed:)) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2023_csv_row(seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2023, seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2023, seed:)) file.rewind end diff --git a/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb b/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb index b0fcaf8b6..b736dc5e5 100644 --- a/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb @@ -15,8 +15,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024)) file.rewind end @@ -38,8 +38,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do file.write("\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024)) file.rewind end @@ -62,8 +62,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024)) file.write("\n") file.rewind end @@ -92,8 +92,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row(seed:)) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row(seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024, seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024, seed:)) file.rewind end @@ -136,7 +136,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do context "when parsing csv without headers" do before do - file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2024)) file.rewind end @@ -155,7 +155,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do before do file.write(bom) - file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2024)) file.rewind end @@ -169,7 +169,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do before do file.write(invalid_sequence) - file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2024)) file.rewind end @@ -186,8 +186,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do file.write("Can be empty?\r") file.write("Type of letting the question applies to\r\n") file.write("Duplicate check field?\r") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024)) file.rewind end @@ -205,8 +205,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024)) file.rewind end @@ -218,7 +218,7 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do context "when without headers using default ordering" do before do - file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2024)) file.rewind end @@ -238,8 +238,8 @@ RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do file.write("Can be empty?\n") file.write("Type of letting the question applies to\n") file.write("Duplicate check field?\n") - file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row(seed:)) - file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row(seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2024, seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2024, seed:)) file.rewind end diff --git a/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb index 33ab390f4..a3392e50d 100644 --- a/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb @@ -1272,6 +1272,19 @@ RSpec.describe BulkUpload::Lettings::Year2024::RowParser do expect(parser.log.rp_dontknow).to be_nil end end + + context "when some reasonable preference options are set as invalid values" do + let(:attributes) { setup_section_params.merge({ bulk_upload:, field_106: "2", field_107: "2", field_108: "3", field_109: "2", field_110: "3", field_111: "-4" }) } + + it "adds errors" do + parser.valid? + expect(parser.errors[:field_107]).to be_present + expect(parser.errors[:field_108]).to be_present + expect(parser.errors[:field_109]).to be_present + expect(parser.errors[:field_110]).to be_present + expect(parser.errors[:field_111]).to be_present + end + end end describe "#field_116" do # referral diff --git a/spec/services/bulk_upload/lettings/year2025/csv_parser_spec.rb b/spec/services/bulk_upload/lettings/year2025/csv_parser_spec.rb new file mode 100644 index 000000000..dcb1cd354 --- /dev/null +++ b/spec/services/bulk_upload/lettings/year2025/csv_parser_spec.rb @@ -0,0 +1,254 @@ +require "rails_helper" + +RSpec.describe BulkUpload::Lettings::Year2025::CsvParser do + subject(:service) { described_class.new(path:) } + + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { build(:lettings_log, :completed) } + + context "when parsing csv with headers" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025)) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when some csv headers are empty (and we don't care about them)" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025)) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with headers with extra rows" do + before do + file.write("Section\n") + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025)) + file.write("\n") + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(8) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + + it "does not parse the last empty row" do + expect(service.row_parsers.count).to eq(1) + end + end + + context "when parsing csv with headers in arbitrary order" do + let(:seed) { rand } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2025, seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025, seed:)) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with extra invalid headers" do + let(:seed) { rand } + let(:log_to_csv) { BulkUpload::LettingsLogToCsv.new(log:) } + let(:field_numbers) { log_to_csv.default_2025_field_numbers + %w[invalid_field_number] } + let(:field_values) { log_to_csv.to_2025_row + %w[value_for_invalid_field_number] } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(log_to_csv.custom_field_numbers_row(seed:, field_numbers:)) + file.write(log_to_csv.to_custom_csv_row(seed:, field_values:)) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + + it "counts the number of valid field numbers correctly" do + expect(service).to be_correct_field_count + end + end + + context "when parsing csv without headers" do + before do + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2025)) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(0) + expect(service.col_offset).to eq(0) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing with BOM aka byte order mark" do + let(:bom) { "\uFEFF" } + + before do + file.write(bom) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2025)) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when an invalid byte sequence" do + let(:invalid_sequence) { "\x81" } + + before do + file.write(invalid_sequence) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2025)) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with carriage returns" do + before do + file.write("Question\r\n") + file.write("Additional info\r") + file.write("Values\r\n") + file.write("Can be empty?\r") + file.write("Type of letting the question applies to\r\n") + file.write("Duplicate check field?\r") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025)) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + describe "#column_for_field", aggregate_failures: true do + context "when with headers using default ordering" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025)) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("F") + expect(service.column_for_field("field_22")).to eql("W") + end + end + + context "when without headers using default ordering" do + before do + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2025)) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("E") + expect(service.column_for_field("field_22")).to eql("V") + end + end + + context "when with headers using custom ordering" do + let(:seed) { 123 } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_field_numbers_row_for_year(2025, seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_year_csv_row(2025, seed:)) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("B") + expect(service.column_for_field("field_22")).to eql("AS") + expect(service.column_for_field("field_26")).to eql("DG") + expect(service.column_for_field("field_25")).to eql("I") + end + end + end +end diff --git a/spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb new file mode 100644 index 000000000..a822d6f2a --- /dev/null +++ b/spec/services/bulk_upload/lettings/year2025/row_parser_spec.rb @@ -0,0 +1,2821 @@ +require "rails_helper" + +RSpec.describe BulkUpload::Lettings::Year2025::RowParser do + subject(:parser) { described_class.new(attributes) } + + let(:now) { Time.zone.local(2025, 4, 5) } + + let(:attributes) { { bulk_upload: } } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: nil, year: 2025) } + let(:user) { create(:user, organisation: owning_org) } + + let(:owning_org) { create(:organisation, :with_old_visible_id) } + let(:managing_org) { create(:organisation, :with_old_visible_id, rent_periods: [4, 1]) } + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:location) { create(:location, :with_old_visible_id, scheme:) } + + let(:setup_section_params) do + { + bulk_upload:, + field_1: owning_org.old_visible_id, + field_2: managing_org.old_visible_id, + field_4: "1", + field_7: "2", + field_8: now.day.to_s, + field_9: now.month.to_s, + field_10: now.strftime("%g"), + field_11: "1", + field_15: "1", + } + end + + before do + allow(FormHandler.instance).to receive(:lettings_in_crossover_period?).and_return(true) + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org) + + LaRentRange.create!( + ranges_rent_id: "1", + la: "E09000008", + beds: 1, + lettype: 7, + soft_min: 12.41, + soft_max: 118.85, + hard_min: 9.87, + hard_max: 200.99, + start_year: 2025, + ) + end + + around do |example| + Timecop.freeze(Date.new(2025, 10, 1)) do + FormHandler.instance.use_real_forms! + example.run + end + Timecop.return + end + + describe "#blank_row?" do + context "when a new object" do + it "returns true" do + expect(parser).to be_blank_row + end + end + + context "when the only populated fields are whitespace" do + before do + parser.field_18 = " " + end + + it "returns true" do + expect(parser).to be_blank_row + end + end + + context "when any field is populated with something other than whitespace" do + before do + parser.field_1 = "1" + end + + it "returns false" do + expect(parser).not_to be_blank_row + end + end + end + + describe "validations" do + before do + stub_request(:get, /api\.postcodes\.io/) + .to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"Manchester\", \"codes\":{\"admin_district\": \"E08000003\"}}}", headers: {}) + + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/find/) + .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: {}) + + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/uprn/) + .to_return(status: 200, body: '{"status":200,"results":[{"DPA":{ + "PO_BOX_NUMBER": "fake", + "ORGANISATION_NAME": "org", + "DEPARTMENT_NAME": "name", + "SUB_BUILDING_NAME": "building", + "BUILDING_NAME": "name", + "BUILDING_NUMBER": "number", + "DEPENDENT_THOROUGHFARE_NAME": "data", + "THOROUGHFARE_NAME": "thing", + "POST_TOWN": "London", + "POSTCODE": "SE2 6RT" + + }}]}', headers: {}) + end + + describe "#valid?" do + context "when the row is blank" do + let(:attributes) { { bulk_upload: } } + + it "returns true" do + expect(parser).to be_valid + end + end + + context "when calling the method multiple times" do + let(:attributes) { { bulk_upload:, field_129: 2 } } + + it "does not add keep adding errors to the pile" do + parser.valid? + expect { parser.valid? }.not_to change(parser.errors, :count) + end + end + + describe "valid/invalid attributes" do + let(:valid_attributes) do + { + bulk_upload:, + field_13: "123", + field_8: now.day.to_s, + field_9: now.month.to_s, + field_10: now.strftime("%g"), + field_23: "EC1N", + field_24: "2TD", + field_1: owning_org.old_visible_id, + field_2: managing_org.old_visible_id, + field_11: "1", + field_7: "2", + field_26: "2", + field_27: "1", + field_28: "1", + field_29: "1", + field_37: "2", + field_38: "1", + field_39: "2", + field_15: "1", + + field_42: "42", + field_48: "41", + field_52: "17", + field_56: "18", + field_60: "16", + field_64: "14", + field_68: "12", + field_72: "20", + + field_43: "F", + field_49: "M", + field_53: "F", + field_57: "M", + field_61: "F", + field_65: "M", + field_69: "F", + field_73: "M", + + field_44: "17", + field_45: "826", + + field_47: "1", + field_51: "2", + field_55: "2", + field_59: "3", + field_63: "2", + field_67: "2", + field_71: "2", + + field_46: "1", + field_50: "2", + field_54: "7", + field_58: "7", + field_62: "8", + field_66: "9", + field_70: "0", + field_74: "10", + + field_75: "1", + field_76: "4", + field_77: "1", + + field_78: "1", + + field_79: "1", + field_80: "0", + field_81: "0", + field_82: "1", + field_83: "0", + + field_85: "2", + + field_96: "11", + field_97: "2", + field_98: "31", + field_100: "3", + field_101: "11", + + field_102: "1", + field_103: "EC1N", + field_104: "2TD", + + field_106: "1", + field_107: "1", + field_108: "", + field_109: "1", + field_110: "", + field_111: "", + + field_112: "1", + field_113: "2", + field_114: "2", + field_115: "2", + + field_116: "2", + + field_117: "1", + field_118: "2", + field_119: "2300", + field_120: "1", + field_121: "4", + + field_123: "4", + field_124: "1234.56", + field_125: "43.32", + field_126: "13.14", + field_127: "101.11", + field_128: "1", + field_129: "34.56", + + field_16: "15", + field_30: now.day.to_s, + field_31: now.month.to_s, + field_32: now.strftime("%g"), + + field_4: "1", + + field_18: "12", + } + end + + context "when valid row" do + before do + allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) + end + + let(:attributes) { valid_attributes } + + it "returns true" do + expect(parser).to be_valid + end + + xit "instantiates a log with everything completed", aggregate_failures: true do + parser.valid? + + questions = parser.send(:questions).reject do |q| + parser.send(:log).optional_fields.include?(q.id) || q.completed?(parser.send(:log)) + end + + expect(questions.map(&:id).size).to eq(0) + expect(questions.map(&:id)).to eql([]) + end + + context "when a general needs log already exists in the db" do + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all (and only) the fields used to determine duplicates" do + parser.valid? + + error_message = I18n.t("validations.lettings.2025.bulk_upload.duplicate") + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_23, # postcode_full + :field_24, # postcode_full + :field_25, # postcode_full + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_124, # brent + :field_125, # scharge + :field_126, # pscharge + :field_127, # supcharg + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_6]).not_to include(error_message) + end + end + + context "when a general needs log already exists in the db but has a different tcharge" do + let(:attributes) { valid_attributes.merge({ field_13: "tenant_code" }) } + + before do + parser.log.save! + saved_log = LettingsLog.find_by(tenancycode: "tenant_code") + saved_log.update!(brent: saved_log.brent + 5) + parser.instance_variable_set(:@valid, nil) + end + + it "is a valid row (and not a duplicate)" do + expect(parser).to be_valid + end + end + + context "when a supported housing log already exists in the db" do + let(:attributes) { valid_attributes.merge({ field_4: "2", field_5: "S#{scheme.id}", field_6: location.old_visible_id, field_36: 3, field_122: 0 }) } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = I18n.t("validations.lettings.2025.bulk_upload.duplicate") + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_124, # brent + :field_125, # scharge + :field_126, # pscharge + :field_127, # supcharg + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_23]).not_to include(error_message) + expect(parser.errors[:field_24]).not_to include(error_message) + expect(parser.errors[:field_25]).not_to include(error_message) + end + end + + context "with old core scheme and location ids" do + context "when a supported housing log already exists in the db" do + let(:attributes) { { bulk_upload:, field_4: "2", field_5: "123" } } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = I18n.t("validations.lettings.2025.bulk_upload.duplicate") + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_124, # brent + :field_125, # scharge + :field_126, # pscharge + :field_127, # supcharg + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_23]).not_to include(error_message) + expect(parser.errors[:field_24]).not_to include(error_message) + expect(parser.errors[:field_25]).not_to include(error_message) + end + end + end + + context "with new core scheme and location ids" do + context "when a supported housing log already exists in the db" do + let(:attributes) { { bulk_upload:, field_4: "2", field_5: "S123" } } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = I18n.t("validations.lettings.2025.bulk_upload.duplicate") + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_23]).not_to include(error_message) + expect(parser.errors[:field_24]).not_to include(error_message) + expect(parser.errors[:field_25]).not_to include(error_message) + end + end + + context "when a supported housing log already exists in the db (2)" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: 2) } + let(:attributes) do + valid_attributes.merge({ field_5: "S#{scheme.id}", + field_4: "2", + field_11: "2", + field_6: location.id, + field_1: owning_org.old_visible_id, + field_122: 0, + field_36: 4 }) + end + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = I18n.t("validations.lettings.2025.bulk_upload.duplicate") + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_122, # household_charge + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_23]).not_to include(error_message) + expect(parser.errors[:field_24]).not_to include(error_message) + expect(parser.errors[:field_25]).not_to include(error_message) + end + end + end + + context "when the rent range validation is triggered but the log has no scheme or location id" do + let(:attributes) do + setup_section_params.merge({ field_5: nil, + field_6: nil, + field_124: 300, + field_123: 1, + field_29: 1, + field_4: 1, + field_11: "2", + field_25: "E09000008" }) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + end + + context "when a hidden log already exists in db" do + before do + parser.log.status = "pending" + parser.log.save! + end + + it "is a valid row" do + expect(parser).to be_valid + end + + it "does not add duplicate errors" do + parser.valid? + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_6, # location + :field_23, # postcode_full + :field_24, # postcode_full + :field_25, # LA + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + ].each do |field| + expect(parser.errors[field]).to be_blank + end + end + end + end + + context "when valid row with valid decimal (integer) field_11" do + before do + allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) + end + + let(:attributes) { valid_attributes.merge(field_11: "1.00") } + + it "returns true" do + expect(parser).to be_valid + end + end + + context "when valid row with invalid decimal (non-integer) field_11" do + before do + allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) + end + + let(:attributes) { valid_attributes.merge(field_11: "1.56") } + + it "returns false" do + expect(parser).not_to be_valid + end + end + + context "with a valid ethnic value" do + context "when field_44 is 20" do + let(:attributes) { valid_attributes.merge({ field_44: "20" }) } + + it "is correctly sets ethnic and ethnic group" do + expect(parser.log.ethnic).to eq(20) + expect(parser.log.ethnic_group).to eq(0) + end + end + end + + context "when the privacy notice is not accepted" do + let(:attributes) { valid_attributes.merge({ field_15: nil }) } + + it "cannot be nulled" do + parser.valid? + expect(parser.errors[:field_15]).to eq(["You must show or give the tenant access to the MHCLG privacy notice before you can submit this log."]) + end + end + + context "when there is a :skip_bu_error error" do + let(:managing_org) { create(:organisation, :with_old_visible_id, rent_periods: [4, 1]) } + let(:attributes) { valid_attributes.merge({ field_123: 3, field_127: 80 }) } + + it "does not add that error" do + parser.valid? + + expect(parser.log.errors.map(&:attribute).sort).to eql(%i[managing_organisation_id period]) + expect(parser.errors.map(&:attribute)).to eql(%i[field_123]) + end + end + end + + describe "#validate_nulls" do + context "when non-setup questions are null" do + let(:attributes) { setup_section_params.merge({ field_43: "" }) } + + it "fetches the question's check_answer_label if it exists" do + parser.valid? + expect(parser.errors[:field_43]).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "lead tenant’s gender identity.")]) + end + end + + context "when other null error is added" do + let(:attributes) { setup_section_params.merge({ field_112: nil }) } + + it "only has one error added to the field" do + parser.valid? + expect(parser.errors[:field_112]).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "was the letting made under the Choice-Based Lettings (CBL)?")]) + end + end + + context "when an invalid value error has been added" do + let(:attributes) { setup_section_params.merge({ field_116: "100" }) } + + it "does not add an additional error" do + parser.valid? + expect(parser.errors[:field_116].length).to eq(1) + expect(parser.errors[:field_116]).to include(match I18n.t("validations.lettings.2025.bulk_upload.invalid_option", question: "")) + end + end + end + end + + context "when setup section not complete" do + let(:attributes) { { bulk_upload:, field_13: "123" } } + + it "has errors on setup fields" do + parser.valid? + + errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute).sort + + expect(errors).to eql(%i[field_1 field_10 field_11 field_15 field_2 field_4 field_7 field_8 field_9]) + end + end + + describe "#field_3" do # assigned_to + context "when blank" do + let(:attributes) { { bulk_upload:, field_3: "", field_4: 1 } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_3]).to be_blank + end + + it "sets assigned to to bulk upload user" do + expect(parser.log.assigned_to).to eq(bulk_upload.user) + end + + it "sets created by to bulk upload user" do + expect(parser.log.created_by).to eq(bulk_upload.user) + end + end + + context "when blank and bulk upload user is support" do + let(:bulk_upload) { create(:bulk_upload, :sales, user: create(:user, :support), year: 2025) } + + let(:attributes) { setup_section_params.merge(bulk_upload:, field_3: nil) } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_3]).to be_present + expect(parser.errors[:field_3]).to include(I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "what is the CORE username of the account this letting log should be assigned to?")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when user could not be found" do + let(:attributes) { { bulk_upload:, field_3: "idonotexist@example.com" } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_3]).to be_present + end + end + + context "when an unaffiliated user" do + let(:other_user) { create(:user) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_3: other_user.email, field_2: managing_org.old_visible_id } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_3]).to be_present + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when a user part of owning org" do + let(:other_user) { create(:user, organisation: owning_org) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_3: other_user.email, field_2: managing_org.old_visible_id } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_3]).to be_blank + end + + it "sets assigned to to the user" do + expect(parser.log.assigned_to).to eq(other_user) + end + + it "sets created by to bulk upload user" do + expect(parser.log.created_by).to eq(bulk_upload.user) + end + end + + context "when email matches other than casing" do + let(:other_user) { create(:user, organisation: owning_org) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_3: other_user.email.upcase!, field_2: managing_org.old_visible_id } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_3]).to be_blank + end + end + + context "when an user part of managing org" do + let(:other_user) { create(:user, organisation: managing_org) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_3: other_user.email, field_2: managing_org.old_visible_id } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_3]).to be_blank + end + end + end + + describe "#field_5, field_6" do # scheme and location fields + context "when nullable not permitted" do + let(:attributes) { { bulk_upload:, field_4: "2", field_11: "2", field_5: nil, field_6: nil } } + + it "cannot be nulled" do + parser.valid? + expect(parser.errors[:field_5]).to eq([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "scheme code.")]) + expect(parser.errors[:field_6]).to eq([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "location code.")]) + end + end + + context "when nullable permitted" do + let(:attributes) { { bulk_upload:, field_4: "1", field_11: "1", field_5: nil, field_6: nil } } + + it "can be nulled" do + parser.valid? + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when using New CORE ids" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let!(:location) { create(:location, :with_old_visible_id, scheme:) } + + before do + parser.valid? + end + + context "when matching scheme cannot be found" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "2", field_5: "S123", field_6: location.id } } + + it "returns a setup error" do + expect(parser.errors.where(:field_5, category: :setup).map(&:message)).to eq([I18n.t("validations.lettings.2025.bulk_upload.scheme.must_relate_to_org")]) + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when missing location" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "2", field_5: "S#{scheme.id}", field_6: nil } } + + it "returns a setup error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors.where(:field_6, category: :setup).map(&:message)).to eq([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "location code.")]) + expect(parser.errors[:field_6].count).to eq(1) + end + end + + context "when matching location cannot be found" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "2", field_5: "S#{scheme.id}", field_6: "123" } } + + it "returns a setup error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors.where(:field_6, category: :setup).map(&:message)).to eq([I18n.t("validations.lettings.2025.bulk_upload.location.must_relate_to_org")]) + end + end + + context "when matching location exists" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "2", field_5: "S#{scheme.id}", field_6: location.id } } + + it "does not return an error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when scheme ID has leading spaces" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "1", field_5: " S#{scheme.id}", field_6: location.id } } + + it "does not return an error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when location exists but not related" do + let(:other_scheme) { create(:scheme, :with_old_visible_id) } + let(:other_location) { create(:location, :with_old_visible_id, scheme: other_scheme) } + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "2", field_5: "S#{scheme.id}", field_6: other_location.id } } + + it "returns a setup error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors.where(:field_6, category: :setup).map(&:message)).to eq([I18n.t("validations.lettings.2025.bulk_upload.location.must_relate_to_org")]) + end + end + + context "when scheme belongs to someone else" do + let(:other_scheme) { create(:scheme, :with_old_visible_id) } + let(:other_location) { create(:location, :with_old_visible_id, scheme: other_scheme) } + let(:attributes) { { bulk_upload:, field_4: "2", field_11: "2", field_5: "S#{other_scheme.id}", field_6: other_location.id, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "returns a setup error" do + expect(parser.errors.where(:field_5, category: :setup).map(&:message)).to eq([I18n.t("validations.lettings.2025.bulk_upload.scheme.must_relate_to_org")]) + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when scheme belongs to owning org" do + let(:attributes) { { bulk_upload:, field_4: "2", field_11: "2", field_5: "S#{scheme.id}", field_6: location.id, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "does not return an error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when scheme belongs to managing org" do + let(:managing_org_scheme) { create(:scheme, :with_old_visible_id, owning_organisation: managing_org) } + let(:managing_org_location) { create(:location, :with_old_visible_id, scheme: managing_org_scheme) } + let(:attributes) { { bulk_upload:, field_4: "2", field_11: "2", field_5: "S#{managing_org_scheme.id}", field_6: managing_org_location.id, field_2: managing_org.old_visible_id } } + + it "clears the scheme answer" do + expect(parser.errors[:field_5]).to include("You must answer scheme name.") + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when matching location exists but is incomplete" do + let(:incomplete_location) { create(:location, :with_old_visible_id, :incomplete, scheme:) } + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "2", field_5: "S#{scheme.id}", field_6: incomplete_location.id } } + + it "returns a setup error for scheme" do + expect(parser.errors.where(:field_5).map(&:message)).to eq(["This location is incomplete. Select another location or update this one."]) + expect(parser.errors.where(:field_6).map(&:message)).to eq(["This location is incomplete. Select another location or update this one."]) + end + end + end + end + + describe "#field_98" do # leaving reason + context "when field_7 is 1 meaning it is a renewal" do + context "when field_98 is 50" do + let(:attributes) { { bulk_upload:, field_98: "50", field_7: "1" } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_98]).to be_blank + end + end + + context "when field_98 is 51" do + let(:attributes) { { bulk_upload:, field_98: "51", field_7: "1" } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_98]).to be_blank + end + end + + context "when field_98 is 52" do + let(:attributes) { { bulk_upload:, field_98: "52", field_7: "1" } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_98]).to be_blank + end + end + + context "when field_98 is 53" do + let(:attributes) { { bulk_upload:, field_98: "53", field_7: "1" } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_98]).to be_blank + end + end + + context "when field_98 is not 50, 51, 52 or 53" do + let(:attributes) { { bulk_upload:, field_98: "1", field_7: "1" } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_98]).to include(I18n.t("validations.lettings.2025.bulk_upload.reason.renewal_reason_needed")) + end + end + end + + context "when no longer a valid option from previous year" do + let(:attributes) { setup_section_params.merge({ field_98: "7" }) } + + it "returns an error" do + parser.valid? + expect(parser.errors[:field_98]).to include(I18n.t("validations.lettings.2025.bulk_upload.invalid_option", question: "what is the tenant’s main reason for the household leaving their last settled home?")) + end + end + end + + describe "#field_79, #field_80, #field_81" do + context "when one item selected" do + let(:attributes) { { bulk_upload:, field_79: "1" } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_79]).to be_blank + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when more than one item selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_80: "1" } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_79]).to be_present + expect(parser.errors[:field_80]).to be_present + end + end + end + + describe "#field_83" do + context "when 1 and another disability field selected" do + let(:attributes) { { bulk_upload:, field_83: "1", field_82: "1" } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_83]).to be_present + end + end + end + + describe "#field_84" do + context "when 1 and another disability field selected" do + let(:attributes) { { bulk_upload:, field_84: "1", field_82: "1" } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_84]).to be_present + end + end + end + + describe "#field_83, #field_84" do + context "when both 1" do + let(:attributes) { { bulk_upload:, field_83: "1", field_84: "1" } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_83]).to be_present + expect(parser.errors[:field_84]).to be_present + end + end + end + + describe "#field_79 - #field_84" do + context "when all blank" do + let(:attributes) { setup_section_params.merge({ field_79: nil, field_80: nil, field_81: nil, field_82: nil, field_83: nil, field_84: nil }) } + + it "adds errors to correct fields" do + parser.valid? + expect(parser.errors[:field_79]).to be_present + expect(parser.errors[:field_80]).to be_present + expect(parser.errors[:field_81]).to be_present + expect(parser.errors[:field_82]).to be_present + expect(parser.errors[:field_83]).to be_present + end + end + + context "when one item selected and field_82 is blank" do + let(:attributes) { setup_section_params.merge({ field_79: "1", field_82: nil }) } + + it "sets other disabled access needs as no" do + parser.valid? + expect(parser.errors[:field_79]).to be_blank + expect(parser.errors[:field_82]).to be_blank + expect(parser.log.housingneeds_other).to eq(0) + end + end + end + + describe "#field_85, field_94 - 99" do + context "when no illness but illnesses answered" do + let(:attributes) { { bulk_upload:, field_85: "2", field_86: "1", field_87: "1", field_88: "1" } } + + it "errors added to correct fields" do + parser.valid? + expect(parser.errors[:field_86]).to be_present + expect(parser.errors[:field_87]).to be_present + expect(parser.errors[:field_88]).to be_present + expect(parser.errors[:field_89]).not_to be_present + expect(parser.errors[:field_90]).not_to be_present + expect(parser.errors[:field_91]).not_to be_present + expect(parser.errors[:field_92]).not_to be_present + expect(parser.errors[:field_93]).not_to be_present + expect(parser.errors[:field_94]).not_to be_present + expect(parser.errors[:field_95]).not_to be_present + end + end + + context "when illness but no illnesses answered" do + let(:attributes) { { bulk_upload:, field_85: "1", field_86: nil, field_87: nil, field_88: nil, field_89: nil, field_90: nil, field_91: nil, field_92: nil, field_93: nil, field_94: nil, field_95: nil } } + + it "errors added to correct fields" do + parser.valid? + expect(parser.errors[:field_86]).to be_present + expect(parser.errors[:field_87]).to be_present + expect(parser.errors[:field_88]).to be_present + expect(parser.errors[:field_89]).to be_present + expect(parser.errors[:field_90]).to be_present + expect(parser.errors[:field_91]).to be_present + expect(parser.errors[:field_92]).to be_present + expect(parser.errors[:field_93]).to be_present + expect(parser.errors[:field_94]).to be_present + expect(parser.errors[:field_95]).to be_present + end + end + end + + describe "#field_112 - 115 (lettings allocation methods)" do + %i[field_112 field_113 field_114 field_115].each do |field| + context "when only #{field} is not given" do + let(:attributes) do + override = {} + override[field] = "" + { bulk_upload:, field_112: "2", field_113: "1", field_114: "2", field_115: "1" }.merge(override) + end + + it "adds an error to #{field}" do + parser.valid? + expect(parser.errors[field]).to be_present + end + end + end + end + + describe "#field_101, field_106 - 15" do + context "when there is a reasonable preference but none is given" do + let(:attributes) { { bulk_upload:, field_106: "1", field_107: nil, field_108: nil, field_109: nil, field_110: nil, field_111: nil } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_107]).to be_present + expect(parser.errors[:field_108]).to be_present + expect(parser.errors[:field_109]).to be_present + expect(parser.errors[:field_110]).to be_present + expect(parser.errors[:field_111]).to be_present + end + end + + context "when some reasonable preference options are set as invalid values" do + let(:attributes) { setup_section_params.merge({ bulk_upload:, field_106: "2", field_107: "2", field_108: "3", field_109: "2", field_110: "3", field_111: "-4" }) } + + it "adds errors" do + parser.valid? + expect(parser.errors[:field_107]).to be_present + expect(parser.errors[:field_108]).to be_present + expect(parser.errors[:field_109]).to be_present + expect(parser.errors[:field_110]).to be_present + expect(parser.errors[:field_111]).to be_present + end + end + end + + describe "#field_116" do # referral + context "when 3 ie PRP nominated by LA and owning org is LA" do + let(:attributes) { { bulk_upload:, field_116: "3", field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_116]).to be_present + end + end + + context "when 4 ie referred by LA and is general needs and owning org is LA" do + let(:attributes) { { bulk_upload:, field_116: "4", field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "1" } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_116]).to be_present + end + end + + context "when 4 ie referred by LA and is general needs and owning org is PRP" do + let(:owning_org) { create(:organisation, :prp, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_116: "4", field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_116]).to be_blank + end + end + + context "when 4 ie referred by LA and is not general needs" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:) } + let(:attributes) { { bulk_upload:, field_116: "4", field_4: "2" } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_116]).to be_blank + end + end + end + + describe "fields 7, 8, 9 => startdate" do + context "when any one of these fields is blank" do + let(:attributes) { { bulk_upload:, field_11: "1", field_8: nil, field_9: nil, field_10: nil } } + + it "returns an error" do + parser.valid? + expect(parser.errors[:field_8]).to be_present + expect(parser.errors[:field_9]).to be_present + expect(parser.errors[:field_10]).to be_present + end + end + + context "when field_10 is 4 digits instead of 2" do + let(:attributes) { setup_section_params.merge({ bulk_upload:, field_10: "2025", field_9: "4", field_8: "5" }) } + + it "correctly sets the date" do + parser.valid? + expect(parser.errors[:field_10]).to be_empty + expect(parser.log.startdate).to eq(Time.zone.local(2025, 4, 5)) + end + end + + context "when field_10 is not 4 or 2 digits" do + let(:attributes) { { bulk_upload:, field_10: "204" } } + + it "returns an error" do + parser.valid? + expect(parser.errors[:field_10]).to include(I18n.t("validations.lettings.2025.bulk_upload.startdate.year_not_two_or_four_digits")) + end + end + + context "when invalid date given" do + let(:attributes) { { bulk_upload:, field_11: "1", field_8: "a", field_9: "12", field_10: "23" } } + + it "does not raise an error" do + expect { parser.valid? }.not_to raise_error + end + end + + context "when inside of collection year" do + let(:attributes) { { bulk_upload:, field_8: "1", field_9: "10", field_10: "25" } } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year: 2025) } + + it "does not return errors" do + parser.valid? + expect(parser.errors[:field_8]).not_to be_present + expect(parser.errors[:field_9]).not_to be_present + expect(parser.errors[:field_10]).not_to be_present + end + end + + context "when outside of collection year" do + around do |example| + Timecop.freeze(Date.new(2024, 4, 2)) do + example.run + end + end + + let(:attributes) { { bulk_upload:, field_8: "1", field_9: "1", field_10: "23" } } + + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year: 2024) } + + it "returns setup errors" do + parser.valid? + expect(parser.errors.where(:field_8, category: :setup)).to be_present + expect(parser.errors.where(:field_9, category: :setup)).to be_present + expect(parser.errors.where(:field_10, category: :setup)).to be_present + end + end + end + + describe "#field_1" do # owning org + context "when blank" do + let(:attributes) { { bulk_upload:, field_1: "", field_4: 1 } } + + it "is not permitted as setup error" do + parser.valid? + expect(parser.errors.where(:field_1, category: :setup).map(&:message)).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "owning organisation.")]) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when cannot find owning org" do + let(:attributes) { { bulk_upload:, field_1: "donotexist" } } + + it "is not permitted as setup error" do + parser.valid? + + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_1 }.message).to eql(I18n.t("validations.lettings.2025.bulk_upload.owning_organisation.not_found")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when org is not stock owning" do + let(:owning_org) { create(:organisation, :with_old_visible_id, :does_not_own_stock) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "is not permitted as setup error" do + parser.valid? + + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_1 }.message).to eql(I18n.t("validations.lettings.2025.bulk_upload.owning_organisation.not_stock_owner")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when not affiliated with owning org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_1: unaffiliated_org.old_visible_id } } + + it "is not permitted as setup error" do + parser.valid? + + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_1 }.message).to eql(I18n.t("validations.lettings.2025.bulk_upload.owning_organisation.not_permitted.not_support")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when user's org has absorbed owning organisation" do + let(:merged_org) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + let(:merged_org_stock_owner) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + + let(:attributes) { { bulk_upload:, field_1: merged_org_stock_owner.old_visible_id, field_2: merged_org.old_visible_id, field_3: user.email } } + + before do + create(:organisation_relationship, parent_organisation: merged_org_stock_owner, child_organisation: merged_org) + merged_org.update!(absorbing_organisation: user.organisation, merge_date: Time.zone.today) + merged_org.reload + user.organisation.reload + end + + it "is permitted" do + parser = described_class.new(attributes) + + parser.valid? + expect(parser.errors.where(:field_1)).not_to be_present + expect(parser.errors.where(:field_3)).not_to be_present + end + end + + context "when user's org has absorbed owning organisation before the startdate" do + let(:merged_org) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + + let(:attributes) { setup_section_params.merge({ field_1: merged_org.old_visible_id, field_2: merged_org.old_visible_id, field_3: user.email }) } + + before do + merged_org.update!(absorbing_organisation: user.organisation, merge_date: Time.zone.today - 5.years) + merged_org.reload + user.organisation.reload + end + + it "is not permitted" do + parser = described_class.new(attributes) + + parser.valid? + expect(parser.errors[:field_1]).to include(/The owning organisation must be active on the tenancy start date/) + expect(parser.errors[:field_2]).to include(/The managing organisation must be active on the tenancy start date/) + expect(parser.errors[:field_8]).to include(/Enter a date when the owning and managing organisation was active/) + expect(parser.errors[:field_9]).to include(/Enter a date when the owning and managing organisation was active/) + expect(parser.errors[:field_10]).to include(/Enter a date when the owning and managing organisation was active/) + end + end + + context "when user is an unaffiliated non-support user and bulk upload organisation is affiliated with the owning organisation" do + let(:affiliated_org) { create(:organisation, :with_old_visible_id) } + let(:unaffiliated_user) { create(:user, organisation: create(:organisation)) } + let(:attributes) { { bulk_upload:, field_1: affiliated_org.old_visible_id } } + let(:organisation_id) { unaffiliated_user.organisation_id } + + before do + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: affiliated_org) + bulk_upload.update!(organisation_id:, user: unaffiliated_user) + end + + it "blocks log creation and adds an error to field_1" do + parser = described_class.new(attributes) + parser.valid? + expect(parser).to be_block_log_creation + expect(parser.errors[:field_1]).to include(I18n.t("validations.lettings.2025.bulk_upload.owning_organisation.not_permitted.not_support")) + end + end + + context "when user is an unaffiliated support user and bulk upload organisation is affiliated with the owning organisation" do + let(:affiliated_org) { create(:organisation, :with_old_visible_id) } + let(:unaffiliated_support_user) { create(:user, :support, organisation: create(:organisation)) } + let(:attributes) { { bulk_upload:, field_1: affiliated_org.old_visible_id } } + let(:organisation_id) { affiliated_org.id } + + before do + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: affiliated_org) + bulk_upload.update!(organisation_id:, user: unaffiliated_support_user) + end + + it "does not block log creation and does not add an error to field_1" do + parser = described_class.new(attributes) + parser.valid? + expect(parser.errors[:field_1]).not_to include(I18n.t("validations.lettings.2025.bulk_upload.owning_organisation.not_permitted")) + end + end + end + + describe "#field_2" do # managing org + context "when blank" do + let(:attributes) { { bulk_upload:, field_2: "", field_4: 1 } } + + it "is not permitted as setup error" do + parser.valid? + + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_2 }.message).to eql(I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "managing organisation.")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when cannot find managing org" do + let(:attributes) { { bulk_upload:, field_2: "donotexist" } } + + it "is not permitted as setup error" do + parser.valid? + + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_2 }.message).to eql(I18n.t("validations.lettings.2025.bulk_upload.managing_organisation.not_found")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when not affiliated with managing org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: unaffiliated_org.old_visible_id } } + + it "is not permitted as setup error" do + parser.valid? + + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_2 }.message).to eql(I18n.t("validations.lettings.2025.bulk_upload.managing_organisation.no_relationship")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + end + + describe "#field_4" do # needs type + context "when blank" do + let(:attributes) { { bulk_upload:, field_4: nil, field_13: "123" } } + + it "is reported as a setup error" do + parser.valid? + expect(parser.errors.where(:field_4, category: :setup).map(&:message)).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "needs type.")]) + end + end + end + + describe "#field_7" do # renewal + context "when blank" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_7: "" } } + + it "has setup errors on the field" do + parser.valid? + expect(parser.errors.where(:field_7, category: :setup).map(&:message)).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "property renewal.")]) + end + end + + context "when none possible option selected" do + let(:attributes) { setup_section_params.merge({ field_7: "101" }) } + + it "adds a setup error" do + parser.valid? + expect(parser.errors.where(:field_7, category: :setup).map(&:message)).to include(I18n.t("validations.lettings.2025.bulk_upload.invalid_option", question: "is this letting a renewal?")) + end + end + end + + describe "UPRN and address fields" do + context "with a general needs log" do + context "when a valid UPRN is given" do + context "and address fields are not given" do + let(:attributes) { setup_section_params.merge({ field_4: 1, field_18: "123456789012" }) } + + it "does not add errors" do + parser.valid? + %i[field_18 field_19 field_20 field_21 field_22 field_23 field_24].each do |field| + expect(parser.errors[field]).to be_empty + end + end + end + end + + context "when an invalid UPRN is given" do + context "and address fields are not given" do + let(:attributes) { setup_section_params.merge({ field_4: 1, field_18: "1234567890123" }) } + + it "adds an appropriate error to the UPRN field" do + parser.valid? + expect(parser.errors[:field_18]).to eql(["UPRN must be 12 digits or less."]) + end + + it "adds errors to missing key address fields" do + parser.valid? + expect(parser.errors[:field_19]).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "address line 1.")]) + expect(parser.errors[:field_21]).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "town or city.")]) + expect(parser.errors[:field_23]).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "part 1 of postcode.")]) + expect(parser.errors[:field_24]).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "part 2 of postcode.")]) + end + end + + context "and address fields are given" do + let(:attributes) { setup_section_params.merge({ field_4: 1, field_18: "1234567890123", field_19: "address line 1", field_21: "town or city", field_23: "AA1", field_24: "1AA" }) } + + it "adds an error to the UPRN field only" do + parser.valid? + expect(parser.errors[:field_18]).to eql(["UPRN must be 12 digits or less."]) + %i[field_19 field_21 field_23 field_24].each do |field| + expect(parser.errors[field]).to be_empty + end + end + + it "does not do an address search" do + parser.valid? + expect(a_request(:any, /api\.os\.uk\/search\/places\/v1\/find/)).not_to have_been_made + end + end + end + + context "when no UPRN is given" do + context "and no address fields are given" do + let(:attributes) { setup_section_params.merge({ field_4: 1 }) } + + it "adds appropriate errors to UPRN and key address fields" do + parser.valid? + expect(parser.errors[:field_18]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_19]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_21]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_23]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_24]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_answered")]) + end + end + + context "and some key address field is missing" do + let(:attributes) { setup_section_params.merge({ field_4: 1, field_21: "town or city", field_23: "AA1", field_24: "1AA" }) } + + it "adds errors to UPRN and the missing key address field" do + parser.valid? + expect(parser.errors[:field_18]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_19]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_21]).to be_empty + expect(parser.errors[:field_23]).to be_empty + expect(parser.errors[:field_24]).to be_empty + end + end + + context "and all key address fields are present" do + let(:attributes) { setup_section_params.merge({ field_4: 1, field_18: nil, field_19: "address line 1", field_21: "town or city", field_23: "AA1", field_24: "1AA" }) } + + context "and an address can be found with a high enough match rating" do + before do + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/find/) + .to_return(status: 200, body: { results: [{ DPA: { MATCH: 0.7, BUILDING_NAME: "", POST_TOWN: "", POSTCODE: "AA1 1AA", UPRN: "1" } }] }.to_json, headers: {}) + end + + it "does not add errors" do + parser.valid? + %i[field_18 field_19 field_20 field_21 field_22 field_23 field_24].each do |field| + expect(parser.errors[field]).to be_empty + end + end + end + + context "when no address can be found" do + before do + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/find/) + .to_return(status: 200, body: { results: [] }.to_json, headers: {}) + end + + it "adds address not found errors to address fields only" do + parser.valid? + expect(parser.errors[:field_18]).to be_empty + %i[field_19 field_20 field_21 field_22 field_23 field_24].each do |field| + expect(parser.errors[field]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_found")]) + end + end + end + + context "when a single address with not a high enough match rating is returned" do + before do + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/find/) + .to_return(status: 200, body: { results: [{ DPA: { MATCH: 0.6, BUILDING_NAME: "", POST_TOWN: "", POSTCODE: "AA1 1AA", UPRN: "1" } }] }.to_json, headers: {}) + end + + it "adds address not found errors to address fields only" do + parser.valid? + expect(parser.errors[:field_18]).to be_empty + %i[field_19 field_20 field_21 field_22 field_23 field_24].each do |field| + expect(parser.errors[field]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_determined.one")]) + end + end + end + + context "when no addresses have a high enough match rating" do + before do + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/find/) + .to_return(status: 200, body: { results: [{ DPA: { MATCH: 0.6, BUILDING_NAME: "", POST_TOWN: "", POSTCODE: "AA1 1AA", UPRN: "1" } }, { DPA: { MATCH: 0.8, BUILDING_NAME: "", POST_TOWN: "", POSTCODE: "BB2 2BB", UPRN: "2" } }] }.to_json, headers: {}) + end + + it "adds address not found errors to address fields only" do + parser.valid? + expect(parser.errors[:field_18]).to be_empty + %i[field_19 field_20 field_21 field_22 field_23 field_24].each do |field| + expect(parser.errors[field]).to eql([I18n.t("validations.lettings.2025.bulk_upload.address.not_determined.multiple")]) + end + end + end + end + end + end + + context "with a supported housing log" do + context "when neither UPRN nor address fields are provided" do + let(:attributes) { setup_section_params.merge({ field_4: 2, field_5: "S#{scheme.id}", field_6: location.old_visible_id, field_18: nil, field_19: nil, field_21: nil, field_23: nil, field_24: nil }) } + + it "does not add missing field errors" do + parser.valid? + %i[field_18 field_19 field_20 field_21 field_22 field_23 field_24].each do |field| + expect(parser.errors[field]).to be_empty + end + end + end + end + end + + describe "#field_17" do # unitletas + context "when no longer a valid option from previous year" do + let(:attributes) { setup_section_params.merge({ field_17: "4" }) } + + it "returns an error" do + parser.valid? + expect(parser.errors[:field_17]).to be_present + end + end + end + + describe "#field_27" do + context "when null" do + let(:attributes) { setup_section_params.merge({ field_27: nil }) } + + it "returns an error" do + parser.valid? + expect(parser.errors[:field_27]).to be_present + end + + it "populates with correct error message" do + parser.valid? + expect(parser.errors[:field_27]).to eql([I18n.t("validations.lettings.2025.bulk_upload.not_answered", question: "type of building.")]) + end + end + end + + describe "#field_48" do # age2 + context "when null but gender given" do + let(:attributes) { setup_section_params.merge({ field_48: "", field_49: "F" }) } + + it "returns an error" do + parser.valid? + expect(parser.errors[:field_48]).to be_present + end + end + end + + describe "#field_45" do + context "when field_45 is a 3 digit nationality code" do + let(:attributes) { setup_section_params.merge({ field_45: "036" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(36) + expect(parser.log.nationality_all_group).to be(12) + end + end + + context "when field_45 is a nationality code without the trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_45: "36" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(36) + expect(parser.log.nationality_all_group).to be(12) + end + end + + context "when field_45 is a nationality code with trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_45: "0036" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(36) + expect(parser.log.nationality_all_group).to be(12) + end + end + + context "when field_45 is 0" do + let(:attributes) { setup_section_params.merge({ field_45: "0" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(0) + expect(parser.log.nationality_all_group).to be(0) + end + end + + context "when field_45 is 000" do + let(:attributes) { setup_section_params.merge({ field_45: "000" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(0) + expect(parser.log.nationality_all_group).to be(0) + end + end + + context "when field_45 is 0000" do + let(:attributes) { setup_section_params.merge({ field_45: "0000" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(0) + expect(parser.log.nationality_all_group).to be(0) + end + end + + context "when field_45 is 826" do + let(:attributes) { setup_section_params.merge({ field_45: "826" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(826) + expect(parser.log.nationality_all_group).to be(826) + end + end + + context "when field_45 is 826 with trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_45: "0826" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(826) + expect(parser.log.nationality_all_group).to be(826) + end + end + + context "when field_45 is not a valid option" do + let(:attributes) { setup_section_params.merge({ field_45: "123123" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(nil) + expect(parser.log.nationality_all_group).to be(nil) + end + + it "adds an error to field_45" do + parser.valid? + expect(parser.errors["field_45"]).to include(I18n.t("validations.lettings.2025.bulk_upload.nationality.invalid")) + end + end + end + + describe "soft validations" do + context "when soft validation is triggered" do + let(:attributes) { setup_section_params.merge({ field_42: 22, field_46: 5 }) } + + it "adds an error to the relevant fields" do + parser.valid? + expect(parser.errors.where(:field_42, category: :soft_validation)).to be_present + expect(parser.errors.where(:field_46, category: :soft_validation)).to be_present + end + + it "populates with correct error message" do + parser.valid? + expect(parser.errors.where(:field_42, category: :soft_validation).first.message).to eql("You told us this person is aged 22 years and retired. The minimum expected retirement age in England is 66.") + expect(parser.errors.where(:field_46, category: :soft_validation).first.message).to eql("You told us this person is aged 22 years and retired. The minimum expected retirement age in England is 66.") + end + end + + context "when a soft validation is triggered that relates both to fields that are and are not routed to" do + let(:attributes) { setup_section_params.merge({ field_78: "1", field_43: "M", field_49: "M", field_53: "M" }) } + + it "adds errors to fields that are routed to" do + parser.valid? + expect(parser.errors.where(:field_49, category: :soft_validation)).to be_present + expect(parser.errors.where(:field_53, category: :soft_validation)).to be_present + end + + it "does not add errors to fields that are not routed to" do + parser.valid? + expect(parser.errors.where(:field_57, category: :soft_validation)).not_to be_present + expect(parser.errors.where(:field_61, category: :soft_validation)).not_to be_present + end + end + + context "when soft validation is triggered and not required" do + let(:attributes) { setup_section_params.merge({ field_124: 120, field_125: 120, field_126: 120, field_127: 120, field_123: 1, field_29: 1, field_4: 1, field_11: "2", field_25: "E09000008" }) } + + it "adds an error to the relevant fields" do + parser.valid? + expect(parser.errors.where(:field_124, category: :soft_validation)).to be_present + end + + it "populates with correct error message" do + parser.valid? + expect(parser.errors.where(:field_124, category: :soft_validation).count).to be(1) + expect(parser.errors.where(:field_124, category: :soft_validation).first.message).to eql("You told us the rent is £120.00 every week. This is higher than we would expect.") + end + end + + context "when an invalid ecstat1 is given" do + let(:attributes) { setup_section_params.merge({ field_46: 11, field_119: 123, field_118: 1 }) } + + it "does not run net income soft validations validation" do + parser.valid? + expect(parser.errors.where(:field_46).count).to be(1) + end + end + end + + describe "log_already_exists?" do + let(:attributes) { { bulk_upload: } } + + before do + build(:lettings_log, owning_organisation: nil, startdate: nil, tenancycode: nil, location: nil, age1: nil, sex1: nil, ecstat1: nil, brent: nil, scharge: nil, pscharge: nil, supcharg: nil).save(validate: false) + end + + it "does not add duplicate logs validation to the blank row" do + expect(parser.log_already_exists?).to eq(false) + end + end + end + + describe "#log" do + describe "#assigned_to" do + context "when blank" do + let(:attributes) { setup_section_params } + + it "takes the user that is uploading" do + expect(parser.log.assigned_to).to eql(bulk_upload.user) + end + end + + context "when email specified" do + let(:other_user) { create(:user, organisation: owning_org) } + + let(:attributes) { setup_section_params.merge(field_3: other_user.email) } + + it "sets to user with specified email" do + expect(parser.log.assigned_to).to eql(other_user) + end + end + end + + describe "#uprn" do + let(:attributes) { { bulk_upload:, field_4: 1, field_18: "12" } } + + it "sets to given value" do + expect(parser.log.uprn).to eql("12") + end + end + + describe "#uprn_known" do + context "when uprn specified" do + let(:attributes) { { bulk_upload:, field_4: 1, field_18: "12" } } + + it "sets to 1" do + expect(parser.log.uprn_known).to be(1) + expect(parser.log.uprn_confirmed).to be(1) + end + end + + context "when uprn blank" do + let(:attributes) { { bulk_upload:, field_4: 1, field_18: "" } } + + it "sets to 0" do + expect(parser.log.uprn_known).to be(0) + end + end + end + + describe "#address_line1" do + let(:attributes) { { bulk_upload:, field_4: 1, field_19: "123 Sesame Street" } } + + it "sets to given value" do + expect(parser.log.address_line1).to eql("123 Sesame Street") + end + end + + describe "#address_line2" do + let(:attributes) { { bulk_upload:, field_4: 1, field_20: "Cookie Town" } } + + it "sets to given value" do + expect(parser.log.address_line2).to eql("Cookie Town") + end + end + + describe "#town_or_city" do + let(:attributes) { { bulk_upload:, field_4: 1, field_21: "London" } } + + it "sets to given value" do + expect(parser.log.town_or_city).to eql("London") + end + end + + describe "#county" do + let(:attributes) { { bulk_upload:, field_4: 1, field_22: "Greater London" } } + + it "sets to given value" do + expect(parser.log.county).to eql("Greater London") + end + end + + describe "address related fields for supported housing logs" do + context "when address data is provided for a supported housing log" do + let(:attributes) { { bulk_upload:, field_4: 2, field_18: nil, field_19: "Flat 1", field_20: "Example Place", field_21: "London", field_22: "Greater London", field_23: "SW1A", field_24: "1AA" } } + + it "is not set on the log" do + expect(parser.log.uprn).to be_nil + expect(parser.log.uprn_known).to be_nil + expect(parser.log.address_line1).to be_nil + expect(parser.log.address_line1_as_entered).to be_nil + expect(parser.log.address_line2).to be_nil + expect(parser.log.address_line2_as_entered).to be_nil + expect(parser.log.town_or_city).to be_nil + expect(parser.log.town_or_city_as_entered).to be_nil + expect(parser.log.county).to be_nil + expect(parser.log.county_as_entered).to be_nil + expect(parser.log.postcode_full).to be_nil + expect(parser.log.postcode_full_as_entered).to be_nil + expect(parser.log.la).to be_nil + expect(parser.log.la_as_entered).to be_nil + expect(parser.log.address_line1_input).to be_nil + expect(parser.log.postcode_full_input).to be_nil + expect(parser.log.select_best_address_match).to be_nil + end + end + end + + [ + %w[age1_known details_known_1 age1 field_42 field_47 field_49], + %w[age2_known details_known_2 age2 field_48 field_47 field_49], + %w[age3_known details_known_3 age3 field_52 field_51 field_53], + %w[age4_known details_known_4 age4 field_56 field_55 field_57], + %w[age5_known details_known_5 age5 field_60 field_59 field_61], + %w[age6_known details_known_6 age6 field_64 field_63 field_65], + %w[age7_known details_known_7 age7 field_68 field_67 field_69], + %w[age8_known details_known_8 age8 field_72 field_71 field_73], + ].each do |known, details_known, age, field, relationship, gender| + describe "##{known} and ##{age}" do + context "when #{field} is blank" do + context "and person details are not given" do + let(:attributes) { { bulk_upload:, field.to_sym => nil, field_4: 1, relationship.to_sym => nil, gender.to_sym => nil } } + + it "does not set ##{known}" do + unless known == "age1_known" + expect(parser.log.public_send(known)).to be_nil + end + end + + it "sets ##{details_known} to no" do + unless details_known == "details_known_1" + expect(parser.log.public_send(details_known)).to eq(1) + end + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end + + context "and person details are given" do + let(:attributes) { { bulk_upload:, field.to_sym => nil, field_4: 1, relationship.to_sym => "C", gender.to_sym => "X" } } + + it "does not set ##{age}" do + parser.valid? + expect(parser.errors[field.to_sym]).to include(/must be a number or the letter R/) + end + end + end + + context "when #{field} is R" do + let(:attributes) { { bulk_upload:, field.to_s => "R" } } + + it "sets ##{known} 1" do + expect(parser.log.public_send(known)).to be(1) + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end + + context "when #{field} is a number" do + let(:attributes) { { bulk_upload:, field.to_s => "50" } } + + it "sets ##{known} to 0" do + expect(parser.log.public_send(known)).to be(0) + end + + it "sets ##{age} to given age" do + expect(parser.log.public_send(age)).to be(50) + end + end + end + end + + describe "#location" do + context "when lookup is via new core id" do + let(:attributes) { { bulk_upload:, field_5: "S#{scheme.id}", field_6: location.id, field_1: "ORG#{owning_org.id}", field_2: "ORG#{owning_org.id}" } } + + it "assigns the correct location" do + expect(parser.log.location).to eql(location) + end + end + end + + describe "#scheme" do + context "when lookup is via new core id" do + let(:attributes) { { bulk_upload:, field_5: "S#{scheme.id}", field_1: "ORG#{owning_org.id}", field_2: "ORG#{owning_org.id}" } } + + it "assigns the correct scheme" do + expect(parser.log.scheme).to eql(scheme) + end + end + end + + describe "#owning_organisation" do + context "when lookup is via id prefixed with ORG" do + let(:attributes) { { bulk_upload:, field_1: "ORG#{owning_org.id}" } } + + it "assigns the correct org" do + expect(parser.log.owning_organisation).to eql(owning_org) + end + end + end + + describe "#managing_organisation" do + context "when lookup is via id prefixed with ORG" do + let(:attributes) { { bulk_upload:, field_2: "ORG#{managing_org.id}" } } + + it "assigns the correct org" do + expect(parser.log.managing_organisation).to eql(managing_org) + end + end + end + + describe "#beds" do + context "when property is a bedsit" do + let(:attributes) { setup_section_params.merge({ field_26: 2, field_29: 2 }) } + + it "sets value to 1 even if field_29 contradicts this" do + expect(parser.log.beds).to be(1) + end + end + + context "when property is not a bedsit" do + let(:attributes) { setup_section_params.merge({ field_26: 1, field_29: 2 }) } + + it "sets value to field_29" do + expect(parser.log.beds).to be(2) + end + end + end + + describe "#cbl" do + context "when field_112 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_112: 1 } } + + it "sets value to 1" do + expect(parser.log.cbl).to be(1) + end + end + + context "when field_112 is no ie 2" do + let(:attributes) { { bulk_upload:, field_112: 2 } } + + it "sets value to 0" do + expect(parser.log.cbl).to be(0) + end + end + + context "when field_112 is not a permitted value" do + let(:attributes) { { bulk_upload:, field_112: 3 } } + + it "adds an error" do + parser.valid? + expect(parser.errors[:field_112]).to include(I18n.t("validations.lettings.2025.bulk_upload.invalid_option", question: "was the letting made under the Choice-Based Lettings (CBL)?")) + end + end + end + + describe "#chr" do + context "when field_114 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_114: 1 } } + + it "sets value to 1" do + expect(parser.log.chr).to be(1) + end + end + + context "when field_114 is no ie 2" do + let(:attributes) { { bulk_upload:, field_114: 2 } } + + it "sets value to 0" do + expect(parser.log.chr).to be(0) + end + end + + context "when field_114 is not a permitted value" do + let(:attributes) { { bulk_upload:, field_114: 3 } } + + it "adds an error" do + parser.valid? + expect(parser.errors[:field_114]).to include(I18n.t("validations.lettings.2025.bulk_upload.invalid_option", question: "was the letting made under the Common Housing Register (CHR)?")) + end + end + end + + describe "#cap" do + context "when field_113 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_113: 1 } } + + it "sets value to 1" do + expect(parser.log.cap).to be(1) + end + end + + context "when field_113 is no ie 2" do + let(:attributes) { { bulk_upload:, field_113: 2 } } + + it "sets value to 0" do + expect(parser.log.cap).to be(0) + end + end + + context "when field_113 is not a permitted value" do + let(:attributes) { { bulk_upload:, field_113: 3 } } + + it "adds an error" do + parser.valid? + expect(parser.errors[:field_113]).to include(I18n.t("validations.lettings.2025.bulk_upload.invalid_option", question: "was the letting made under the Common Allocation Policy (CAP)?")) + end + end + end + + describe "#accessible_register" do + context "when field_115 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_115: 1 } } + + it "sets value to 1" do + expect(parser.log.accessible_register).to be(1) + end + end + + context "when field_115 is no ie 2" do + let(:attributes) { { bulk_upload:, field_115: 2 } } + + it "sets value to 0" do + expect(parser.log.accessible_register).to be(0) + end + end + + context "when field_115 is not a permitted value" do + let(:attributes) { { bulk_upload:, field_115: 3 } } + + it "adds an error" do + parser.valid? + expect(parser.errors[:field_115]).to include(I18n.t("validations.lettings.2025.bulk_upload.invalid_option", question: "was the letting made under the Accessible Register?")) + end + end + end + + describe "#letting_allocation_unknown" do + context "when field_112, 113, 114, 115 are no ie 2" do + let(:attributes) { { bulk_upload:, field_112: 2, field_113: 2, field_114: 2, field_115: 2 } } + + it "sets value to 1" do + expect(parser.log.letting_allocation_unknown).to be(1) + end + end + + context "when any one of field_112, 113, 114, 115 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_115: 1 } } + + it "sets value to 0" do + expect(parser.log.letting_allocation_unknown).to be(0) + end + end + end + + describe "#renewal" do + context "when field_7 is no ie 2" do + let(:attributes) { { bulk_upload:, field_7: 2 } } + + it "sets value to 0" do + expect(parser.log.renewal).to eq(0) + end + end + end + + describe "#sexN fields" do + let(:attributes) do + { + bulk_upload:, + field_43: "F", + field_49: "M", + field_53: "X", + field_57: "R", + field_61: "F", + field_65: "M", + field_69: "X", + field_73: "R", + } + end + + it "sets value from correct mapping" do + expect(parser.log.sex1).to eql("F") + expect(parser.log.sex2).to eql("M") + expect(parser.log.sex3).to eql("X") + expect(parser.log.sex4).to eql("R") + expect(parser.log.sex5).to eql("F") + expect(parser.log.sex6).to eql("M") + expect(parser.log.sex7).to eql("X") + expect(parser.log.sex8).to eql("R") + end + end + + describe "#ecstatN fields" do + let(:attributes) do + { + bulk_upload:, + field_46: "1", + field_50: "2", + field_54: "6", + field_58: "7", + field_62: "8", + field_66: "9", + field_70: "0", + field_74: "10", + } + end + + it "sets value from correct mapping", aggregate_failures: true do + expect(parser.log.ecstat1).to eq(1) + expect(parser.log.ecstat2).to eq(2) + expect(parser.log.ecstat3).to eq(6) + expect(parser.log.ecstat4).to eq(7) + expect(parser.log.ecstat5).to eq(8) + expect(parser.log.ecstat6).to eq(9) + expect(parser.log.ecstat7).to eq(0) + expect(parser.log.ecstat8).to eq(10) + end + end + + describe "#relatN fields" do + let(:attributes) do + { + bulk_upload:, + field_47: "1", + field_51: "2", + field_55: "2", + field_59: "3", + field_63: "1", + field_67: "2", + field_71: "2", + } + end + + it "sets value from correct mapping", aggregate_failures: true do + expect(parser.log.relat2).to eq("P") + expect(parser.log.relat3).to eq("X") + expect(parser.log.relat4).to eq("X") + expect(parser.log.relat5).to eq("R") + expect(parser.log.relat6).to eq("P") + expect(parser.log.relat7).to eq("X") + expect(parser.log.relat8).to eq("X") + end + end + + describe "#net_income_known" do + context "when 1" do + let(:attributes) { { bulk_upload:, field_117: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.net_income_known).to eq(0) + end + end + + context "when 2" do + let(:attributes) { { bulk_upload:, field_117: "2" } } + + it "sets value from correct mapping" do + expect(parser.log.net_income_known).to eq(1) + end + end + + context "when 3" do + let(:attributes) { { bulk_upload:, field_117: "3" } } + + it "sets value from correct mapping" do + expect(parser.log.net_income_known).to eq(2) + end + end + end + + describe "#unitletas" do + let(:attributes) { { bulk_upload:, field_17: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.unitletas).to eq(1) + end + end + + describe "#rsnvac" do + let(:attributes) { { bulk_upload:, field_16: "5" } } + + it "sets value from correct mapping" do + expect(parser.log.rsnvac).to eq(5) + end + end + + describe "#sheltered" do + let(:attributes) { { bulk_upload:, field_36: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.sheltered).to eq(1) + end + end + + describe "illness fields" do + mapping = [ + { attribute: :illness_type_1, field: :field_94 }, + { attribute: :illness_type_2, field: :field_88 }, + { attribute: :illness_type_3, field: :field_91 }, + { attribute: :illness_type_4, field: :field_86 }, + { attribute: :illness_type_5, field: :field_87 }, + { attribute: :illness_type_6, field: :field_89 }, + { attribute: :illness_type_7, field: :field_90 }, + { attribute: :illness_type_8, field: :field_93 }, + { attribute: :illness_type_9, field: :field_92 }, + { attribute: :illness_type_10, field: :field_95 }, + ] + + mapping.each do |hash| + describe "##{hash[:attribute]}" do + context "when yes" do + let(:attributes) { { bulk_upload:, hash[:field] => "1" } } + + it "sets value from correct mapping" do + expect(parser.log.public_send(hash[:attribute])).to eq(1) + end + end + + context "when no" do + let(:attributes) { { bulk_upload:, hash[:field] => "", field_4: 1 } } + + it "sets value from correct mapping" do + expect(parser.log.public_send(hash[:attribute])).to be_nil + end + end + end + end + end + + describe "#irproduct_other" do + let(:attributes) { { bulk_upload:, field_12: "some other product" } } + + it "sets value to given free text string" do + expect(parser.log.irproduct_other).to eql("some other product") + end + end + + describe "#tenancyother" do + let(:attributes) { { bulk_upload:, field_40: "some other tenancy" } } + + it "sets value to given free text string" do + expect(parser.log.tenancyother).to eql("some other tenancy") + end + end + + describe "#tenancylength" do + let(:attributes) { { bulk_upload:, field_41: "2" } } + + it "sets value to given free text string" do + expect(parser.log.tenancylength).to eq(2) + end + end + + describe "#earnings" do + let(:attributes) { { bulk_upload:, field_119: "104.50" } } + + it "rounds to the nearest whole pound" do + expect(parser.log.earnings).to eq(105) + end + end + + describe "#reasonother" do + context "when reason is 'other'" do + let(:attributes) { { bulk_upload:, field_98: "20", field_99: "some other reason" } } + + it "is set to given free text string" do + expect(parser.log.reasonother).to eql("some other reason") + end + end + + context "when reason is not 'other'" do + let(:attributes) { { bulk_upload:, field_98: "50", field_99: "some other reason" } } + + it "is set to nil" do + expect(parser.log.reasonother).to be_nil + end + end + end + + describe "#ppcodenk" do + let(:attributes) { { bulk_upload:, field_102: "2" } } + + it "sets correct value from mapping" do + expect(parser.log.ppcodenk).to eq(1) + end + end + + describe "#household_charge" do + context "when log is general needs" do + let(:attributes) { { bulk_upload:, field_4: 1, field_122: "1" } } + + it "sets correct value from mapping" do + expect(parser.log.household_charge).to eq(nil) + end + end + + context "when log is supported housing" do + let(:attributes) { { bulk_upload:, field_4: 2, field_122: "1" } } + + it "sets correct value from mapping" do + expect(parser.log.household_charge).to eq(1) + end + end + end + + describe "#supcharg" do + let(:attributes) { setup_section_params.merge({ field_124: "330", field_125: "0", field_126: "0", field_127: "123.45" }) } + + it "sets value given" do + expect(parser.log.supcharg).to eq(123.45) + end + + context "when other charges are not given" do + let(:attributes) { setup_section_params.merge({ field_127: "123.45", field_124: nil, field_125: nil, field_126: nil }) } + + it "does not set charges values" do + parser.log.save! + expect(parser.log.tcharge).to be_nil + expect(parser.log.brent).to be_nil + expect(parser.log.supcharg).to be_nil + expect(parser.log.pscharge).to be_nil + expect(parser.log.scharge).to be_nil + end + + it "adds an error to all missing charges" do + parser.valid? + expect(parser.errors[:field_124]).to eql([I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "basic rent")]) + expect(parser.errors[:field_125]).to eql([I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "service charge")]) + expect(parser.errors[:field_126]).to eql([I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "personal service charge")]) + expect(parser.errors[:field_127]).to eql([I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "basic rent"), I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "service charge"), I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "personal service charge")]) + end + end + + context "when supscharg is not given" do + let(:attributes) { setup_section_params.merge({ field_123: 1, field_124: "350.45", field_125: "0", field_126: "0", field_127: nil }) } + + it "does not set charges values" do + parser.log.save! + expect(parser.log.period).not_to be_nil + expect(parser.log.tcharge).to be_nil + expect(parser.log.brent).to be_nil + expect(parser.log.supcharg).to be_nil + expect(parser.log.pscharge).to be_nil + expect(parser.log.scharge).to be_nil + end + + it "adds an error to all charges" do + parser.valid? + expect(parser.errors[:field_124]).to eql([I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "support charge")]) + expect(parser.errors[:field_125]).to eql([I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "support charge")]) + expect(parser.errors[:field_126]).to eql([I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "support charge")]) + expect(parser.errors[:field_127]).to eql([I18n.t("validations.lettings.2025.bulk_upload.charges.missing_charges", sentence_fragment: "support charge")]) + end + end + end + + describe "#pscharge" do + let(:attributes) { { bulk_upload:, field_124: "111.45", field_125: "0", field_126: "123.45", field_127: "0" } } + + it "sets value given" do + expect(parser.log.pscharge).to eq(123.45) + end + end + + describe "#scharge" do + let(:attributes) { { bulk_upload:, field_124: "111.45", field_125: "123.45", field_126: "0", field_127: "0" } } + + it "sets value given" do + expect(parser.log.scharge).to eq(123.45) + end + end + + describe "#propcode" do + let(:attributes) { { bulk_upload:, field_14: "abc123" } } + + it "sets value given" do + expect(parser.log.propcode).to eq("abc123") + end + end + + describe "#mrcdate" do + context "when valid" do + let(:attributes) { { bulk_upload:, field_33: "13", field_34: "12", field_35: "22" } } + + it "sets value given" do + expect(parser.log.mrcdate).to eq(Date.new(2022, 12, 13)) + end + end + + context "when valid (4 digit year)" do + let(:attributes) { { bulk_upload:, field_33: "13", field_34: "12", field_35: "2022" } } + + it "sets value given" do + expect(parser.log.mrcdate).to eq(Date.new(2022, 12, 13)) + end + end + + context "when invalid" do + let(:attributes) { { bulk_upload:, field_33: "13", field_34: "13", field_35: "22" } } + + it "does not raise an error" do + expect { parser.log.mrcdate }.not_to raise_error + end + end + end + + describe "#majorrepairs" do + context "when mrcdate given" do + let(:attributes) { { bulk_upload:, field_33: "13", field_34: "12", field_35: "22" } } + + it "sets #majorrepairs to 1" do + expect(parser.log.majorrepairs).to eq(1) + end + end + + context "when mrcdate not given" do + let(:attributes) { { bulk_upload:, field_33: "", field_34: "", field_35: "", field_4: 1 } } + + it "sets #majorrepairs to 0" do + expect(parser.log.majorrepairs).to eq(0) + end + end + end + + describe "#voiddate" do + context "when valid" do + let(:attributes) { { bulk_upload:, field_30: "13", field_31: "12", field_32: "22" } } + + it "sets value given" do + expect(parser.log.voiddate).to eq(Date.new(2022, 12, 13)) + end + end + + context "when valid (4 digit year)" do + let(:attributes) { { bulk_upload:, field_30: "13", field_31: "12", field_32: "2022" } } + + it "sets value given" do + expect(parser.log.voiddate).to eq(Date.new(2022, 12, 13)) + end + end + + context "when invalid" do + let(:attributes) { { bulk_upload:, field_30: "13", field_31: "13", field_32: "22" } } + + it "does not raise an error" do + expect { parser.log.voiddate }.not_to raise_error + end + end + end + + describe "#startdate" do + let(:attributes) { { bulk_upload:, field_8: now.day.to_s, field_9: now.month.to_s, field_10: now.strftime("%g") } } + + it "sets value given" do + expect(parser.log.startdate).to eq(now) + end + end + + describe "#postcode_full" do + let(:attributes) { { bulk_upload:, field_4: 1, field_23: " EC1N ", field_24: " 2TD " } } + + it "strips whitespace" do + expect(parser.log.postcode_full).to eql("EC1N 2TD") + end + + context "when a partial postcode is provided" do + let(:attributes) { { bulk_upload:, field_4: 1, field_23: "EC1N", field_24: "" } } + + it "is set to the partial value" do + expect(parser.log.postcode_full).to eql("EC1N") + end + end + end + + describe "#la" do + let(:attributes) { { bulk_upload:, field_4: 1, field_25: "E07000223" } } + + it "sets to given value" do + expect(parser.log.la).to eql("E07000223") + end + end + + describe "#prevloc" do + let(:attributes) { { bulk_upload:, field_105: "E07000223" } } + + it "sets to given value" do + expect(parser.log.prevloc).to eql("E07000223") + end + end + + describe "#previous_la_known" do + context "when known" do + let(:attributes) { { bulk_upload:, field_105: "E07000223" } } + + it "sets to 1" do + expect(parser.log.previous_la_known).to eq(1) + end + end + + context "when not known" do + let(:attributes) { { bulk_upload:, field_105: "", field_4: 1 } } + + it "sets to 0" do + expect(parser.log.previous_la_known).to eq(0) + end + end + end + + describe "#first_time_property_let_as_social_housing" do + context "when field_16 is 15, 16, or 17" do + let(:attributes) { { bulk_upload:, field_16: %w[15 16 17].sample } } + + it "sets to 1" do + expect(parser.log.first_time_property_let_as_social_housing).to eq(1) + end + end + + context "when field_16 is not 15, 16, or 17" do + let(:attributes) { { bulk_upload:, field_16: "1" } } + + it "sets to 0" do + expect(parser.log.first_time_property_let_as_social_housing).to eq(0) + end + end + end + + describe "#housingneeds" do + context "when no disabled needs" do + let(:attributes) { { bulk_upload:, field_83: "1" } } + + it "sets to 2" do + expect(parser.log.housingneeds).to eq(2) + end + end + + context "when dont know about disabled needs" do + let(:attributes) { { bulk_upload:, field_84: "1" } } + + it "sets to 3" do + expect(parser.log.housingneeds).to eq(3) + end + end + + context "when housingneeds are given" do + let(:attributes) { { bulk_upload:, field_83: "0", field_81: "1", field_82: "1" } } + + it "sets correct housingneeds" do + expect(parser.log.housingneeds).to eq(1) + expect(parser.log.housingneeds_type).to eq(2) + expect(parser.log.housingneeds_other).to eq(1) + end + end + + context "when housingneeds are given and field_82 is nil" do + let(:attributes) { { bulk_upload:, field_83: nil, field_81: "1", field_82: "1" } } + + it "sets correct housingneeds" do + expect(parser.log.housingneeds).to eq(1) + expect(parser.log.housingneeds_type).to eq(2) + expect(parser.log.housingneeds_other).to eq(1) + end + end + + context "when housingneeds are not given" do + let(:attributes) { { bulk_upload:, field_79: nil, field_80: nil, field_81: nil, field_83: nil } } + + it "sets correct housingneeds" do + expect(parser.log.housingneeds).to eq(1) + expect(parser.log.housingneeds_type).to eq(3) + end + end + + context "when housingneeds a and b are selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_80: "1" } } + + it "sets error on housingneeds a and b" do + parser.valid? + expect(parser.errors[:field_79]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds_type.only_one_option_permitted")) + expect(parser.errors[:field_80]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds_type.only_one_option_permitted")) + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when housingneeds a and c are selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_81: "1" } } + + it "sets error on housingneeds a and c" do + parser.valid? + expect(parser.errors[:field_79]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds_type.only_one_option_permitted")) + expect(parser.errors[:field_81]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds_type.only_one_option_permitted")) + expect(parser.errors[:field_80]).to be_blank + end + end + + context "when housingneeds b and c are selected" do + let(:attributes) { { bulk_upload:, field_80: "1", field_81: "1" } } + + it "sets error on housingneeds b and c" do + parser.valid? + expect(parser.errors[:field_80]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds_type.only_one_option_permitted")) + expect(parser.errors[:field_81]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds_type.only_one_option_permitted")) + expect(parser.errors[:field_79]).to be_blank + end + end + + context "when housingneeds a and g are selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_83: "1" } } + + it "sets error on housingneeds a and g" do + parser.valid? + expect(parser.errors[:field_83]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds.no_disabled_needs_conjunction")) + expect(parser.errors[:field_79]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds.no_disabled_needs_conjunction")) + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when only housingneeds g is selected" do + let(:attributes) { { bulk_upload:, field_79: "0", field_83: "1" } } + + it "does not add any housingneeds errors" do + parser.valid? + expect(parser.errors[:field_55]).to be_blank + expect(parser.errors[:field_79]).to be_blank + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when housingneeds a and h are selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_84: "1" } } + + it "sets error on housingneeds a and h" do + parser.valid? + expect(parser.errors[:field_84]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds.dont_know_disabled_needs_conjunction")) + expect(parser.errors[:field_79]).to include(I18n.t("validations.lettings.2025.bulk_upload.housingneeds.dont_know_disabled_needs_conjunction")) + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when only housingneeds h is selected" do + let(:attributes) { { bulk_upload:, field_79: "0", field_84: "1" } } + + it "does not add any housingneeds errors" do + parser.valid? + expect(parser.errors[:field_84]).to be_blank + expect(parser.errors[:field_79]).to be_blank + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + end + + describe "#housingneeds_type" do + context "when field_79 is 1" do + let(:attributes) { { bulk_upload:, field_79: "1" } } + + it "set to 0" do + expect(parser.log.housingneeds_type).to eq(0) + end + end + + context "when field_80 is 1" do + let(:attributes) { { bulk_upload:, field_80: "1" } } + + it "set to 1" do + expect(parser.log.housingneeds_type).to eq(1) + end + end + + context "when field_81 is 1" do + let(:attributes) { { bulk_upload:, field_81: "1" } } + + it "set to 2" do + expect(parser.log.housingneeds_type).to eq(2) + end + end + end + + describe "#housingneeds_other" do + context "when field_54 is 1" do + let(:attributes) { { bulk_upload:, field_82: "1" } } + + it "sets to 1" do + expect(parser.log.housingneeds_other).to eq(1) + end + end + end + end + + describe "#startdate" do + context "when year of 9 is passed to represent 2009" do + let(:attributes) { { bulk_upload:, field_8: "1", field_9: "1", field_10: "9" } } + + it "uses the year 2009" do + expect(parser.send(:startdate)).to eql(Date.new(2009, 1, 1)) + end + end + end + + describe "#spreadsheet_duplicate_hash" do + it "returns a hash" do + expect(parser.spreadsheet_duplicate_hash).to be_a(Hash) + end + end + + describe "#add_duplicate_found_in_spreadsheet_errors" do + it "adds errors" do + expect { parser.add_duplicate_found_in_spreadsheet_errors }.to change(parser.errors, :size) + end + end +end diff --git a/spec/services/bulk_upload/sales/log_creator_spec.rb b/spec/services/bulk_upload/sales/log_creator_spec.rb index 2353a74c6..51c90781f 100644 --- a/spec/services/bulk_upload/sales/log_creator_spec.rb +++ b/spec/services/bulk_upload/sales/log_creator_spec.rb @@ -5,162 +5,187 @@ RSpec.describe BulkUpload::Sales::LogCreator do let(:owning_org) { create(:organisation, old_visible_id: 123) } let(:user) { create(:user, organisation: owning_org) } - - let(:bulk_upload) { create(:bulk_upload, :sales, user:, year: 2024) } - let(:csv_parser) { instance_double(BulkUpload::Sales::Year2024::CsvParser) } - let(:row_parser) { instance_double(BulkUpload::Sales::Year2024::RowParser) } let(:log) { build(:sales_log, :completed, assigned_to: user, owning_organisation: owning_org, managing_organisation: owning_org) } - before do - allow(BulkUpload::Sales::Year2024::CsvParser).to receive(:new).and_return(csv_parser) - allow(csv_parser).to receive(:row_parsers).and_return([row_parser]) - allow(row_parser).to receive(:log).and_return(log) - allow(row_parser).to receive(:bulk_upload=).and_return(true) - allow(row_parser).to receive(:valid?).and_return(true) - allow(row_parser).to receive(:blank_row?).and_return(false) - end - - describe "#call" do - context "when a valid csv with new log" do - it "creates a new log" do - expect { service.call }.to change(SalesLog, :count) - end - - it "create a log with pending status" do - service.call - expect(SalesLog.last.status).to eql("pending") - end - - it "associates log with bulk upload" do - service.call - - log = SalesLog.last - expect(log.bulk_upload).to eql(bulk_upload) - expect(bulk_upload.sales_logs).to include(log) - end - - it "sets the creation method" do - service.call + [2023, 2024, 2025].each do |year| + context "when #{year}" do + let(:bulk_upload) { create(:bulk_upload, :sales, user:, year:) } + let(:year_csv_parser) { instance_double("BulkUpload::Sales::Year#{year}::CsvParser".constantize) } + let(:year_row_parser) { instance_double("BulkUpload::Sales::Year#{year}::RowParser".constantize) } - expect(SalesLog.last.creation_method_bulk_upload?).to be true - end - end - - context "when a valid csv with several blank rows" do before do - allow(row_parser).to receive(:blank_row?).and_return(true) - end - - it "ignores them and does not create the logs" do - expect { service.call }.not_to change(SalesLog, :count) - end - end - - context "when a valid csv with row with one invalid non setup field" do - let(:log) do - build( - :sales_log, - :completed, - age1: 5, - owning_organisation: owning_org, - assigned_to: user, - managing_organisation: owning_org, - ) - end - - it "creates the log" do - expect { service.call }.to change(SalesLog, :count).by(1) - end - - it "blanks invalid field" do - service.call - - record = SalesLog.last - expect(record.age1).to be_blank - end - end - - context "when a valid csv with row with compound errors on non setup field" do - let(:log) do - build( - :sales_log, - :completed, - owning_organisation: owning_org, - assigned_to: user, - managing_organisation: owning_org, - ownershipsch: 2, - value: 200_000, - deposit: 10_000, - mortgageused: 1, - mortgage: 100_000, - grant: 10_000, - ) + allow("BulkUpload::Sales::Year#{year}::CsvParser".constantize).to receive(:new).and_return(year_csv_parser) + allow(year_csv_parser).to receive(:row_parsers).and_return([year_row_parser]) + allow(year_row_parser).to receive(:log).and_return(log) + allow(year_row_parser).to receive(:bulk_upload=).and_return(true) + allow(year_row_parser).to receive(:valid?).and_return(true) + allow(year_row_parser).to receive(:blank_row?).and_return(false) end - it "creates the log" do - expect { service.call }.to change(SalesLog, :count).by(1) - end - - it "blanks invalid field" do - service.call - - record = SalesLog.last - expect(record.value).to be_blank - expect(record.deposit).to be_blank - expect(record.mortgage).to be_blank - expect(record.grant).to be_blank - end - end - - context "when pre-creating logs" do - it "creates a new log" do + it "creates a parser for the correct year" do + # This would fail without parser stubs, so the parser must be for the expected year expect { service.call }.to change(SalesLog, :count) end - - it "creates a log with correct states" do - service.call - - last_log = SalesLog.last - - expect(last_log.status).to eql("pending") - expect(last_log.status_cache).to eql("completed") - end end + end - context "when valid csv with existing log" do - xit "what should happen?" + # Apart from picking the correct year's parser, everything else is year-independent + context "when 2024" do + let(:bulk_upload) { create(:bulk_upload, :sales, user:, year: 2024) } + let(:csv_parser) { instance_double(BulkUpload::Sales::Year2024::CsvParser) } + let(:row_parser) { instance_double(BulkUpload::Sales::Year2024::RowParser) } + + before do + allow(BulkUpload::Sales::Year2024::CsvParser).to receive(:new).and_return(csv_parser) + allow(csv_parser).to receive(:row_parsers).and_return([row_parser]) + allow(row_parser).to receive(:log).and_return(log) + allow(row_parser).to receive(:bulk_upload=).and_return(true) + allow(row_parser).to receive(:valid?).and_return(true) + allow(row_parser).to receive(:blank_row?).and_return(false) end - context "with a valid csv and soft validations" do - let(:log) do - build( - :sales_log, - :completed, - age1: 30, - age1_known: 0, - ecstat1: 5, - owning_organisation: owning_org, - assigned_to: user, - managing_organisation: owning_org, - ) - end - - it "creates a new log" do - expect { service.call }.to change(SalesLog, :count) - end - - it "creates a log with pending status" do - service.call - expect(SalesLog.last.status).to eql("pending") - end + describe "#call" do + context "when a valid csv with new log" do + it "creates a new log" do + expect { service.call }.to change(SalesLog, :count) + end + + it "create a log with pending status" do + service.call + expect(SalesLog.last.status).to eql("pending") + end - it "does not set unanswered soft validations" do - service.call + it "associates log with bulk upload" do + service.call - log = SalesLog.last - expect(log.age1).to be(30) - expect(log.ecstat1).to be(5) - expect(log.retirement_value_check).to be(nil) + log = SalesLog.last + expect(log.bulk_upload).to eql(bulk_upload) + expect(bulk_upload.sales_logs).to include(log) + end + + it "sets the creation method" do + service.call + + expect(SalesLog.last.creation_method_bulk_upload?).to be true + end + end + + context "when a valid csv with several blank rows" do + before do + allow(row_parser).to receive(:blank_row?).and_return(true) + end + + it "ignores them and does not create the logs" do + expect { service.call }.not_to change(SalesLog, :count) + end + end + + context "when a valid csv with row with one invalid non setup field" do + let(:log) do + build( + :sales_log, + :completed, + age1: 5, + owning_organisation: owning_org, + assigned_to: user, + managing_organisation: owning_org, + ) + end + + it "creates the log" do + expect { service.call }.to change(SalesLog, :count).by(1) + end + + it "blanks invalid field" do + service.call + + record = SalesLog.last + expect(record.age1).to be_blank + end + end + + context "when a valid csv with row with compound errors on non setup field" do + let(:log) do + build( + :sales_log, + :completed, + owning_organisation: owning_org, + assigned_to: user, + managing_organisation: owning_org, + ownershipsch: 2, + value: 200_000, + deposit: 10_000, + mortgageused: 1, + mortgage: 100_000, + grant: 10_000, + ) + end + + it "creates the log" do + expect { service.call }.to change(SalesLog, :count).by(1) + end + + it "blanks invalid field" do + service.call + + record = SalesLog.last + expect(record.value).to be_blank + expect(record.deposit).to be_blank + expect(record.mortgage).to be_blank + expect(record.grant).to be_blank + end + end + + context "when pre-creating logs" do + it "creates a new log" do + expect { service.call }.to change(SalesLog, :count) + end + + it "creates a log with correct states" do + service.call + + last_log = SalesLog.last + + expect(last_log.status).to eql("pending") + expect(last_log.status_cache).to eql("completed") + end + end + + context "when valid csv with existing log" do + xit "what should happen?" + end + + context "with a valid csv and soft validations" do + let(:log) do + build( + :sales_log, + :completed, + age1: 30, + age1_known: 0, + ecstat1: 5, + owning_organisation: owning_org, + assigned_to: user, + managing_organisation: owning_org, + ) + end + + it "creates a new log" do + expect { service.call }.to change(SalesLog, :count) + end + + it "creates a log with pending status" do + service.call + expect(SalesLog.last.status).to eql("pending") + end + + it "does not set unanswered soft validations" do + service.call + + log = SalesLog.last + expect(log.age1).to be(30) + expect(log.ecstat1).to be(5) + expect(log.retirement_value_check).to be(nil) + end end end end diff --git a/spec/services/bulk_upload/sales/validator_spec.rb b/spec/services/bulk_upload/sales/validator_spec.rb index 71a94a725..6bd118344 100644 --- a/spec/services/bulk_upload/sales/validator_spec.rb +++ b/spec/services/bulk_upload/sales/validator_spec.rb @@ -58,6 +58,22 @@ RSpec.describe BulkUpload::Sales::Validator do end end + context "when trying to upload 2024 logs for 2025 bulk upload" do + let(:bulk_upload) { create(:bulk_upload, user:, year: 2025) } + let(:log) { build(:sales_log, :completed, saledate: Time.zone.local(2024, 10, 10), assigned_to: user) } + + before do + file.write(log_to_csv.default_field_numbers_row_for_year(2025)) + file.write(log_to_csv.to_year_csv_row(2025)) + file.rewind + end + + it "is not valid" do + expect(validator).not_to be_valid + expect(validator.errors["base"]).to eql([I18n.t("validations.sales.2025.bulk_upload.wrong_template.wrong_template")]) + end + end + [ { line_ending: "\n", name: "unix" }, { line_ending: "\r\n", name: "windows" }, diff --git a/spec/services/bulk_upload/sales/year2025/csv_parser_spec.rb b/spec/services/bulk_upload/sales/year2025/csv_parser_spec.rb new file mode 100644 index 000000000..849e6bf2f --- /dev/null +++ b/spec/services/bulk_upload/sales/year2025/csv_parser_spec.rb @@ -0,0 +1,191 @@ +require "rails_helper" + +RSpec.describe BulkUpload::Sales::Year2025::CsvParser do + subject(:service) { described_class.new(path:) } + + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { build(:sales_log, :completed, :with_uprn) } + + context "when parsing csv with headers" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025)) + file.write("\n") + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_16).to eql(log.uprn) + end + + it "counts the number of valid field numbers correctly" do + expect(service).to be_correct_field_count + end + + it "does not parse the last empty row" do + expect(service.row_parsers.count).to eq(1) + end + end + + context "when some csv headers are empty (and we don't care about them)" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025)) + file.write("\n") + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_16).to eql(log.uprn) + end + + it "counts the number of valid field numbers correctly" do + expect(service).to be_correct_field_count + end + + it "does not parse the last empty row" do + expect(service.row_parsers.count).to eq(1) + end + end + + context "when parsing csv with headers in arbitrary order" do + let(:seed) { rand } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025, seed:)) + file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025, seed:)) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_16).to eql(log.uprn) + end + end + + context "when parsing csv without headers" do + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { build(:sales_log, :completed, :with_uprn) } + + before do + file.write(BulkUpload::SalesLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2025)) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(0) + expect(service.col_offset).to eq(0) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_16).to eql(log.uprn) + end + end + + context "when parsing with BOM aka byte order mark" do + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { build(:sales_log, :completed, :with_uprn) } + let(:bom) { "\uFEFF" } + + before do + file.write(bom) + file.write(BulkUpload::SalesLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2025)) + file.close + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_16).to eql(log.uprn) + end + end + + context "when an invalid byte sequence" do + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { build(:sales_log, :completed, :with_uprn) } + let(:invalid_sequence) { "\x81" } + + before do + file.write(invalid_sequence) + file.write(BulkUpload::SalesLogToCsv.new(log:, col_offset: 0).to_year_csv_row(2025)) + file.close + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_16).to eql(log.uprn) + end + end + + describe "#column_for_field", aggregate_failures: true do + context "when headers present" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025)) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_1")).to eql("B") + expect(service.column_for_field("field_99")).to eql("CV") + end + end + end + + context "when parsing csv with carriage returns" do + before do + file.write("Question\r\n") + file.write("Additional info\r") + file.write("Values\r\n") + file.write("Can be empty?\r") + file.write("Type of letting the question applies to\r\n") + file.write("Duplicate check field?\r") + file.write(BulkUpload::SalesLogToCsv.new(log:).default_field_numbers_row_for_year(2025)) + file.write(BulkUpload::SalesLogToCsv.new(log:).to_year_csv_row(2025)) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_16).to eql(log.uprn) + end + end +end diff --git a/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb new file mode 100644 index 000000000..a0b87cdc1 --- /dev/null +++ b/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb @@ -0,0 +1,1915 @@ +require "rails_helper" + +RSpec.describe BulkUpload::Sales::Year2025::RowParser do + subject(:parser) { described_class.new(attributes) } + + let(:now) { Time.zone.parse("01/05/2025") } + + let(:attributes) { { bulk_upload: } } + let(:bulk_upload) { create(:bulk_upload, :sales, user:, year: 2025) } + let(:user) { create(:user, organisation: owning_org) } + let(:owning_org) { create(:organisation, :with_old_visible_id) } + let(:managing_org) { create(:organisation, :with_old_visible_id) } + + let(:setup_section_params) do + { + bulk_upload:, + field_1: now.day.to_s, # sale day + field_2: now.month.to_s, # sale month + field_3: now.strftime("%g"), # sale year + field_4: owning_org.old_visible_id, # organisation + field_5: managing_org.old_visible_id, # organisation + field_6: user.email, # user + field_7: "test id", # purchase id + field_8: "1", # owhershipsch + field_9: "2", # shared ownership sale type + field_10: "1", # staircasing + field_12: "2", # joint purchase + field_14: "1", # noint + field_15: "1", # privacy notice + } + end + let(:valid_attributes) do + { + bulk_upload:, + field_1: "12", + field_2: "5", + field_3: "25", + field_4: owning_org.old_visible_id, + field_5: managing_org.old_visible_id, + field_7: "test id", + field_8: "1", + field_9: "2", + field_10: "1", + field_12: "1", + field_13: "2", + field_14: "1", + field_15: "1", + field_16: "12", + field_17: "Address line 1", + field_21: "CR0", + field_22: "4BB", + field_23: "E09000008", + field_24: "1", + field_25: "2", + field_26: "1", + field_27: "3", + field_28: "32", + field_29: "M", + field_30: "12", + field_31: "28", + field_32: "1", + field_33: "1", + field_34: "R", + field_35: "32", + field_36: "F", + field_37: "17", + field_38: "28", + field_39: "2", + field_40: "1", + field_41: "0", + field_58: "1", + field_59: "1", + field_60: "A1", + field_61: "1AA", + field_62: "E09000008", + field_63: "3", + field_65: "3", + field_67: "5", + field_68: "3", + field_69: "3", + field_70: "30000", + field_71: "1", + field_72: "15000", + field_73: "1", + field_74: "4", + field_75: "20000", + field_76: "3", + field_79: "5", + field_80: "24", + field_81: "3", + field_82: "2022", + field_83: "1", + field_84: "1", + field_85: "1", + field_107: "250000", + field_108: "25", + field_109: "1", + field_89: "5000", + field_90: "20", + field_96: "10", + field_97: "40", + field_98: "1", + field_99: "2", + field_94: "200", + field_91: "20000", + field_111: "800", + field_100: "05", + field_101: "04", + field_102: "2020", + field_103: "4", + field_104: "06", + field_105: "07", + field_106: "2023", + field_110: "900", + } + end + + around do |example| + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org) + + Timecop.freeze(Time.zone.local(2026, 2, 22)) do + Singleton.__init__(FormHandler) + example.run + end + end + + describe "#blank_row?" do + context "when a new object" do + it "returns true" do + expect(parser).to be_blank_row + end + end + + context "when any field is populated" do + before do + parser.field_4 = "1" + end + + it "returns false" do + expect(parser).not_to be_blank_row + end + end + + context "when the only populated fields are empty strings or whitespace" do + before do + parser.field_6 = " " + parser.field_25 = "" + end + + it "returns true" do + expect(parser).to be_blank_row + end + end + end + + describe "purchaser_code" do + before do + def purch_id_field + described_class::QUESTIONS.key("What is the purchaser code?").to_s + end + end + + let(:attributes) do + { + bulk_upload:, + purch_id_field => "some purchaser code", + } + end + + it "is linked to the correct field" do + expect(parser.purchaser_code).to eq("some purchaser code") + end + end + + describe "previous postcode known" do + context "when field_59 is 1" do + let(:attributes) do + { + bulk_upload:, + field_59: 1, + } + end + + it "sets previous postcode known to yes" do + expect(parser.log.ppcodenk).to eq(0) + end + end + + context "when field_59 is 2" do + let(:attributes) do + { + bulk_upload:, + field_59: 2, + } + end + + it "sets previous postcode known to no" do + expect(parser.log.ppcodenk).to eq(1) + end + end + end + + describe "income and savings fields" do + context "when set to R" do + let(:attributes) do + { + bulk_upload:, + field_70: "R", # income 1 + field_72: "R", # income 2 + field_75: "R", # savings + } + end + + it "sets the not known field as not known" do + expect(parser.log.income1nk).to be(1) + expect(parser.log.income2nk).to be(1) + expect(parser.log.savingsnk).to be(1) + end + + it "leaves the value field nil" do + expect(parser.log.income1).to be_nil + expect(parser.log.income2).to be_nil + expect(parser.log.savings).to be_nil + end + end + + context "when set to a number" do + let(:attributes) do + { + bulk_upload:, + field_70: "30000", # income 1 + field_72: "0", # income 2 + field_75: "12420", # savings + } + end + + it "sets the not known field as known" do + expect(parser.log.income1nk).to be(0) + expect(parser.log.income2nk).to be(0) + expect(parser.log.savingsnk).to be(0) + end + + it "sets the values" do + expect(parser.log.income1).to be(30_000) + expect(parser.log.income2).to be(0) + expect(parser.log.savings).to be(12_420) + end + end + end + + describe "validations" do + before do + stub_request(:get, /api\.postcodes\.io/) + .to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"Manchester\", \"codes\":{\"admin_district\": \"E08000003\"}}}", headers: {}) + + 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 + + describe "#valid?" do + context "when the row is blank" do + let(:attributes) { { bulk_upload: } } + + it "returns true" do + expect(parser).to be_valid + end + end + + context "when calling the method multiple times" do + let(:attributes) { { bulk_upload:, field_8: 2 } } + + it "does not add keep adding errors to the pile" do + parser.valid? + expect { parser.valid? }.not_to change(parser.errors, :count) + end + end + + context "when valid row" do + let(:attributes) { valid_attributes } + + it "returns true" do + expect(parser).to be_valid + end + + it "instantiates a log with everything completed", aggregate_failures: true do + parser.valid? + + questions = parser.send(:questions).reject do |q| + parser.send(:log).optional_fields.include?(q.id) || q.completed?(parser.send(:log)) + end + + expect(questions.map(&:id).size).to eq(0) + expect(questions.map(&:id)).to eql([]) + end + end + + describe "#validate_nulls" do + context "when non-setup questions are null" do + let(:attributes) { setup_section_params.merge({ field_29: "" }) } + + it "fetches the question's check_answer_label if it exists" do + parser.valid? + expect(parser.errors[:field_29]).to eql([I18n.t("validations.not_answered", question: "buyer 1’s gender identity.")]) + end + end + + context "when other null error is added" do + let(:attributes) { setup_section_params.merge({ field_17: nil }) } + + it "only has one error added to the field" do + parser.valid? + expect(parser.errors[:field_17]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_answered")]) + end + end + + context "when an invalid value error has been added" do + let(:attributes) { setup_section_params.merge({ field_10: "2", field_32: "100" }) } + + it "does not add an additional error" do + parser.valid? + expect(parser.errors[:field_32].length).to eq(1) + expect(parser.errors[:field_32]).to include(match I18n.t("validations.sales.2025.bulk_upload.invalid_option", question: "")) + end + end + end + end + + context "when setup section not complete and type is not given" do + let(:attributes) do + { + bulk_upload:, + field_7: "test id", + } + end + + it "has errors on correct setup fields" do + parser.valid? + + errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute).sort + + expect(errors).to eql(%i[field_1 field_14 field_15 field_2 field_3 field_4 field_5 field_8]) + end + end + + context "when setup section not complete and type is shared ownership" do + let(:attributes) do + { + bulk_upload:, + field_7: "test id", + field_8: "1", + } + end + + it "has errors on correct setup fields" do + parser.valid? + + errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute).sort + + expect(errors).to eql(%i[field_1 field_10 field_12 field_14 field_15 field_2 field_3 field_4 field_5 field_9]) + end + end + + context "when setup section not complete it's shared ownership joint purchase" do + let(:attributes) do + { + bulk_upload:, + field_7: "test id", + field_8: "1", + field_9: "2", + field_12: "1", + } + end + + it "has errors on correct setup fields" do + parser.valid? + + errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute).sort + + expect(errors).to eql(%i[field_1 field_10 field_13 field_14 field_15 field_2 field_3 field_4 field_5]) + end + end + + context "when setup section not complete and type is discounted ownership" do + let(:attributes) do + { + bulk_upload:, + field_7: "test id", + field_8: "2", + field_11: nil, + } + end + + it "has errors on correct setup fields" do + parser.valid? + + errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute).sort + + expect(errors).to eql(%i[field_1 field_11 field_12 field_14 field_15 field_2 field_3 field_4 field_5]) + end + end + + context "when setup section not complete and it's discounted ownership joint purchase" do + let(:attributes) do + { + bulk_upload:, + field_28: "2", + field_43: "8", + field_36: "1", + } + end + + it "has errors on correct setup fields" do + parser.valid? + + errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute).sort + + expect(errors).to eql(%i[field_1 field_14 field_15 field_2 field_3 field_4 field_5 field_8]) + end + end + + describe "#field_4" do # owning org + context "when no data given" do + let(:attributes) { setup_section_params.merge(field_4: nil) } + + it "is not permitted as setup error" do + parser.valid? + expect(parser.errors.where(:field_4, category: :setup).map(&:message)).to eql([I18n.t("validations.sales.2025.bulk_upload.not_answered", question: "owning organisation.")]) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when cannot find owning org" do + let(:attributes) { { bulk_upload:, field_4: "donotexist" } } + + it "is not permitted as a setup error" do + parser.valid? + expect(parser.errors.where(:field_4, category: :setup).map(&:message)).to eql([I18n.t("validations.sales.2025.bulk_upload.owning_organisation.not_found")]) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when not affiliated with owning org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_4: unaffiliated_org.old_visible_id } } + + it "is not permitted as setup error" do + parser.valid? + expect(parser.errors.where(:field_4, category: :setup).map(&:message)).to eql([I18n.t("validations.sales.2025.bulk_upload.owning_organisation.not_permitted.not_support")]) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when user's org has absorbed owning organisation with stock owners" do + let(:merged_org) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + let(:merged_org_stock_owner) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + + let(:attributes) { { bulk_upload:, field_4: merged_org_stock_owner.old_visible_id } } + + before do + create(:organisation_relationship, parent_organisation: merged_org_stock_owner, child_organisation: merged_org) + merged_org.update!(absorbing_organisation: user.organisation, merge_date: Time.zone.today) + merged_org.reload + user.organisation.reload + end + + it "is permitted" do + parser.valid? + expect(parser.errors.where(:field_4)).not_to be_present + end + end + + context "when user's org has absorbed owning organisation" do + let(:merged_org) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + + let(:attributes) { { bulk_upload:, field_4: merged_org.old_visible_id, field_6: user.email } } + + before do + merged_org.update!(absorbing_organisation: user.organisation, merge_date: Time.zone.today) + merged_org.reload + user.organisation.reload + user.reload + end + + it "is permitted" do + parser = described_class.new(attributes) + + parser.valid? + expect(parser.errors.where(:field_4)).not_to be_present + expect(parser.errors.where(:field_6)).not_to be_present + end + end + + context "when user's org has absorbed owning organisation before the startdate" do + let(:merged_org) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + + let(:attributes) { setup_section_params.merge({ field_4: merged_org.old_visible_id, field_6: user.email }) } + + before do + merged_org.update!(absorbing_organisation: user.organisation, merge_date: Time.zone.today - 3.years) + merged_org.reload + user.organisation.reload + user.reload + end + + it "is not permitted" do + parser = described_class.new(attributes) + + parser.valid? + expect(parser.errors[:field_4]).to include(/The owning organisation must be active on the sale completion date/) + expect(parser.errors[:field_1]).to include(/Enter a date when the owning organisation was active/) + expect(parser.errors[:field_2]).to include(/Enter a date when the owning organisation was active/) + expect(parser.errors[:field_3]).to include(/Enter a date when the owning organisation was active/) + end + end + + context "when user is an unaffiliated non-support user and bulk upload organisation is affiliated with the owning organisation" do + let(:affiliated_org) { create(:organisation, :with_old_visible_id) } + let(:unaffiliated_user) { create(:user, organisation: create(:organisation)) } + let(:attributes) { { bulk_upload:, field_4: affiliated_org.old_visible_id } } + let(:organisation_id) { unaffiliated_user.organisation_id } + + before do + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: affiliated_org) + bulk_upload.update!(organisation_id:, user: unaffiliated_user) + end + + it "blocks log creation and adds an error to field_4" do + parser = described_class.new(attributes) + parser.valid? + expect(parser).to be_block_log_creation + expect(parser.errors[:field_4]).to include(I18n.t("validations.sales.2025.bulk_upload.owning_organisation.not_permitted.not_support")) + end + end + + context "when user is an unaffiliated support user and bulk upload organisation is affiliated with the owning organisation" do + let(:affiliated_org) { create(:organisation, :with_old_visible_id) } + let(:unaffiliated_support_user) { create(:user, :support, organisation: create(:organisation)) } + let(:attributes) { { bulk_upload:, field_4: affiliated_org.old_visible_id } } + let(:organisation_id) { affiliated_org.id } + + before do + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: affiliated_org) + bulk_upload.update!(organisation_id:, user: unaffiliated_support_user) + end + + it "does not block log creation and does not add an error to field_4" do + parser = described_class.new(attributes) + parser.valid? + expect(parser.errors[:field_4]).not_to include(I18n.t("validations.sales.2025.bulk_upload.owning_organisation.not_permitted.not_support")) + end + end + end + + describe "#field_6" do # username for assigned_to + context "when blank" do + let(:attributes) { setup_section_params.merge(bulk_upload:, field_6: nil) } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_6]).to be_blank + end + + it "sets assigned to to bulk upload user" do + parser.valid? + expect(parser.log.assigned_to).to eq(bulk_upload.user) + end + + it "sets created by to bulk upload user" do + parser.valid? + expect(parser.log.created_by).to eq(bulk_upload.user) + end + end + + context "when blank and bulk upload user is support" do + let(:bulk_upload) { create(:bulk_upload, :sales, user: create(:user, :support), year: 2025) } + + let(:attributes) { setup_section_params.merge(bulk_upload:, field_6: nil) } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_6]).to be_present + expect(parser.errors[:field_6]).to include(I18n.t("validations.sales.2025.bulk_upload.not_answered", question: "what is the CORE username of the account this sales log should be assigned to?")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when user could not be found" do + let(:attributes) { { bulk_upload:, field_6: "idonotexist@example.com" } } + + it "is not permitted" do + parser.valid? + expect(parser.errors[:field_6]).to be_present + end + end + + context "when an unaffiliated user" do + let(:other_user) { create(:user) } + + let(:attributes) { { bulk_upload:, field_4: owning_org.old_visible_id, field_6: other_user.email } } + + it "is not permitted as a setup error" do + parser.valid? + expect(parser.errors.where(:field_6, category: :setup)).to be_present + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when a user part of owning org" do + let(:other_user) { create(:user, organisation: owning_org) } + + let(:attributes) { { bulk_upload:, field_4: owning_org.old_visible_id, field_6: other_user.email } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_6]).to be_blank + end + + it "sets assigned to to the user" do + parser.valid? + expect(parser.log.assigned_to).to eq(other_user) + end + + it "sets created by to bulk upload user" do + parser.valid? + expect(parser.log.created_by).to eq(bulk_upload.user) + end + end + + context "when email matches other than casing" do + let(:other_user) { create(:user, organisation: owning_org) } + + let(:attributes) { { bulk_upload:, field_4: owning_org.old_visible_id, field_6: other_user.email.upcase! } } + + it "is permitted" do + parser.valid? + expect(parser.errors[:field_6]).to be_blank + end + end + end + + describe "fields 1, 2, 3 => saledate" do + context "when all of these fields are blank" do + let(:attributes) { setup_section_params.merge({ field_1: nil, field_2: nil, field_3: nil }) } + + it "returns them as setup errors" do + parser.valid? + expect(parser.errors.where(:field_1, category: :setup)).to be_present + expect(parser.errors.where(:field_2, category: :setup)).to be_present + expect(parser.errors.where(:field_3, category: :setup)).to be_present + end + end + + context "when one of these fields is blank" do + let(:attributes) { setup_section_params.merge({ field_1: "1", field_2: "1", field_3: nil }) } + + it "returns an error only on blank field as setup error" do + parser.valid? + expect(parser.errors[:field_1]).to be_blank + expect(parser.errors[:field_2]).to be_blank + expect(parser.errors.where(:field_3, category: :setup)).to be_present + end + end + + context "when field 3 is 4 digits instead of 2" do + let(:attributes) { setup_section_params.merge({ bulk_upload:, field_3: "2025" }) } + + it "correctly sets the date" do + parser.valid? + expect(parser.errors.where(:field_3, category: :setup)).to be_empty + expect(parser.log.saledate).to eq(Time.zone.local(2025, 5, 1)) + end + end + + context "when field 2 is not 2 or 4 digits" do + let(:attributes) { setup_section_params.merge({ bulk_upload:, field_3: "202" }) } + + it "returns a setup error" do + parser.valid? + expect(parser.errors.where(:field_3, category: :setup).map(&:message)).to include(I18n.t("validations.sales.2025.bulk_upload.saledate.year_not_two_or_four_digits")) + end + end + + context "when invalid date given" do + let(:attributes) { setup_section_params.merge({ field_1: "a", field_2: "12", field_3: "2023" }) } + + it "does not raise an error" do + expect { parser.valid? }.not_to raise_error + end + end + + context "when inside of collection year" do + around do |example| + Timecop.freeze(Date.new(2025, 10, 1)) do + example.run + end + end + + let(:attributes) { setup_section_params.merge({ field_1: "1", field_2: "10", field_3: "25" }) } + + let(:bulk_upload) { create(:bulk_upload, :sales, user:, year: 2025) } + + it "does not return errors" do + parser.valid? + expect(parser.errors[:field_1]).not_to be_present + expect(parser.errors[:field_2]).not_to be_present + expect(parser.errors[:field_3]).not_to be_present + end + end + + context "when outside of collection year" do + around do |example| + Timecop.freeze(Date.new(2023, 4, 2)) do + example.run + end + end + + let(:attributes) { setup_section_params.merge({ field_1: "1", field_2: "1", field_3: "22" }) } + + let(:bulk_upload) { create(:bulk_upload, :sales, user:, year: 2023) } + + it "returns setup errors" do + parser.valid? + expect(parser.errors.where(:field_1, category: :setup)).to be_present + expect(parser.errors.where(:field_2, category: :setup)).to be_present + expect(parser.errors.where(:field_3, category: :setup)).to be_present + end + end + end + + context "when the log already exists in the db" do + let(:attributes) { valid_attributes } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all (and only) the fields used to determine duplicates" do + parser.valid? + + error_message = I18n.t("validations.sales.2025.bulk_upload.duplicate") + + [ + :field_4, # Owning org + :field_1, # Sale completion date + :field_2, # Sale completion date + :field_3, # Sale completion date + :field_21, # Postcode + :field_22, # Postcode + :field_28, # Buyer 1 age + :field_29, # Buyer 1 gender + :field_32, # Buyer 1 working situation + :field_7, # Purchaser code + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + end + end + + context "when a hidden log already exists in db" do + before do + parser.log.status = "pending" + parser.log.save! + end + + it "is a valid row" do + expect(parser).to be_valid + end + + it "does not add duplicate errors" do + parser.valid? + + [ + :field_4, # Owning org + :field_1, # Sale completion date + :field_2, # Sale completion date + :field_3, # Sale completion date + :field_21, # Postcode + :field_22, # Postcode + :field_28, # Buyer 1 age + :field_29, # Buyer 1 gender + :field_32, # Buyer 1 working situation + :field_7, # Purchaser code + ].each do |field| + expect(parser.errors[field]).to be_blank + end + end + end + + describe "#field_8" do # ownership scheme + context "when an invalid option" do + let(:attributes) { setup_section_params.merge({ field_8: "100" }) } + + it "returns setup error" do + parser.valid? + expect(parser.errors.where(:field_8, category: :setup)).to be_present + end + end + end + + describe "#field_9" do # type for shared ownership sale + context "when an invalid option" do + let(:attributes) { setup_section_params.merge({ field_9: "100" }) } + + it "returns setup error" do + parser.valid? + expect(parser.errors.where(:field_9, category: :setup)).to be_present + end + end + end + + describe "#field_11" do # type for discounted sale + context "when an invalid option" do + let(:attributes) { setup_section_params.merge({ field_11: "100" }) } + + it "returns setup error" do + parser.valid? + expect(parser.errors.where(:field_11, category: :setup)).to be_present + end + end + end + + describe "#field_115" do # percentage discount + context "when percentage discount over 70" do + let(:attributes) { valid_attributes.merge({ field_8: "2", field_10: "2", field_11: "9", field_115: "71" }) } + + it "returns correct error" do + parser.valid? + expect(parser.errors.where(:field_115).map(&:message)).to include(I18n.t("validations.sales.2025.bulk_upload.numeric.within_range", field: "Percentage discount", min: "0%", max: "70%")) + end + end + + context "when percentage discount not over 70" do + let(:attributes) { valid_attributes.merge({ field_8: "2", field_10: "2", field_115: "70" }) } + + it "does not return error" do + parser.valid? + expect(parser.errors.where(:field_115)).not_to be_present + end + end + + context "when percentage less than 0" do + let(:attributes) { valid_attributes.merge({ field_8: "2", field_10: "2", field_115: "-1" }) } + + it "returns correct error" do + parser.valid? + expect(parser.errors.where(:field_115).map(&:message)).to include(I18n.t("validations.sales.2025.bulk_upload.numeric.within_range", field: "Percentage discount", min: "0%", max: "70%")) + end + end + end + + describe "#field_12" do # joint purchase + context "when an invalid option" do + let(:attributes) { setup_section_params.merge({ field_12: "100" }) } + + it "returns a setup error" do + parser.valid? + expect(parser.errors.where(:field_12, category: :setup)).to be_present + end + end + end + + describe "#field_13" do # more than 2 joint buyers? + context "when invalid option and must be answered" do + let(:attributes) { setup_section_params.merge({ field_12: "1", field_13: "100" }) } + + it "returns a setup error" do + parser.valid? + expect(parser.errors.where(:field_13, category: :setup)).to be_present + end + end + end + + describe "UPRN and address fields" do + context "when a valid UPRN is given" do + let(:attributes) { setup_section_params.merge({ field_16: "12" }) } + + it "does not add errors" do + parser.valid? + %i[field_16 field_17 field_18 field_19 field_20 field_21 field_22].each do |field| + expect(parser.errors[field]).to be_empty + end + end + + it "sets UPRN, UPRN known, and UPRN confirmed" do + expect(parser.log.uprn).to eq("12") + expect(parser.log.uprn_known).to eq(1) + expect(parser.log.uprn_confirmed).to eq(1) + end + end + + context "when an invalid UPRN is given" do + context "and address fields are not given" do + let(:attributes) { setup_section_params.merge({ field_16: "1234567890123" }) } + + it "adds an appropriate error to the UPRN field" do + parser.valid? + expect(parser.errors[:field_16]).to eql(["UPRN must be 12 digits or less."]) + end + + it "adds errors to missing key address fields" do + parser.valid? + expect(parser.errors[:field_17]).to eql([I18n.t("validations.sales.2025.bulk_upload.not_answered", question: "address line 1.")]) + expect(parser.errors[:field_19]).to eql([I18n.t("validations.sales.2025.bulk_upload.not_answered", question: "town or city.")]) + expect(parser.errors[:field_21]).to eql([I18n.t("validations.sales.2025.bulk_upload.not_answered", question: "part 1 of postcode.")]) + expect(parser.errors[:field_22]).to eql([I18n.t("validations.sales.2025.bulk_upload.not_answered", question: "part 2 of postcode.")]) + end + end + + context "and address fields are given" do + let(:attributes) { setup_section_params.merge({ field_16: "1234567890123", field_17: "address line 1", field_19: "town or city", field_21: "AA1", field_22: "1AA" }) } + + it "adds an error to the UPRN field only" do + parser.valid? + expect(parser.errors[:field_16]).to eql(["UPRN must be 12 digits or less."]) + %i[field_17 field_19 field_21 field_22].each do |field| + expect(parser.errors[field]).to be_empty + end + end + + it "does not do an address search" do + parser.valid? + expect(a_request(:any, /api\.os\.uk\/search\/places\/v1\/find/)).not_to have_been_made + end + end + end + + context "when no UPRN is given" do + context "and no address fields are given" do + let(:attributes) { setup_section_params } + + it "adds appropriate errors to UPRN and key address fields" do + parser.valid? + expect(parser.errors[:field_16]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_17]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_19]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_21]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_22]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_answered")]) + end + end + + context "and some key address field is missing" do + let(:attributes) { setup_section_params.merge({ field_19: "town or city", field_21: "AA1", field_22: "1AA" }) } + + it "adds errors to UPRN and the missing key address field" do + parser.valid? + expect(parser.errors[:field_16]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_17]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_19]).to be_empty + expect(parser.errors[:field_21]).to be_empty + expect(parser.errors[:field_22]).to be_empty + end + end + + context "and all key address fields are present" do + let(:attributes) { setup_section_params.merge({ field_16: nil, field_17: "address line 1", field_19: "town or city", field_21: "AA1", field_22: "1AA" }) } + + context "and an address can be found with a high enough match rating" do + before do + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/find/) + .to_return(status: 200, body: { results: [{ DPA: { MATCH: 0.7, BUILDING_NAME: "", POST_TOWN: "", POSTCODE: "AA1 1AA", UPRN: "1" } }] }.to_json, headers: {}) + end + + it "does not add errors" do + parser.valid? + %i[field_16 field_17 field_18 field_19 field_20 field_21 field_22].each do |field| + expect(parser.errors[field]).to be_empty + end + end + end + + context "when no address can be found" do + before do + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/find/) + .to_return(status: 200, body: { results: [] }.to_json, headers: {}) + end + + it "adds address not found errors to address fields only" do + parser.valid? + expect(parser.errors[:field_16]).to be_empty + %i[field_17 field_18 field_19 field_20 field_21 field_22].each do |field| + expect(parser.errors[field]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_found")]) + end + end + end + + context "when no address has a high enough match rating" do + before do + stub_request(:get, /api\.os\.uk\/search\/places\/v1\/find/) + .to_return(status: 200, body: { results: [{ DPA: { MATCH: 0.6, BUILDING_NAME: "", POST_TOWN: "", POSTCODE: "AA1 1AA", UPRN: "1" } }] }.to_json, headers: {}) + end + + it "adds address not found errors to address fields only" do + parser.valid? + expect(parser.errors[:field_16]).to be_empty + %i[field_17 field_18 field_19 field_20 field_21 field_22].each do |field| + expect(parser.errors[field]).to eql([I18n.t("validations.sales.2025.bulk_upload.address.not_determined")]) + end + end + end + end + end + end + + describe "#field_15" do # data protection + let(:attributes) { setup_section_params.merge({ field_15: nil }) } + + before do + parser.valid? + end + + context "when not answered" do + it "returns a setup error" do + expect(parser.errors.where(:field_15, category: :setup)).to be_present + end + end + + context "when the privacy notice is not accepted" do + it "cannot be nulled" do + expect(parser.errors[:field_15]).to eq(["You must show or give the buyer access to the MHCLG privacy notice before you can submit this log."]) + end + end + end + + [ + %w[age1_known details_known_1 age1 field_28 field_34 field_36], + %w[age2_known details_known_2 age2 field_35 field_34 field_36], + %w[age3_known details_known_3 age3 field_43 field_42 field_44], + %w[age4_known details_known_4 age4 field_47 field_46 field_48], + %w[age5_known details_known_5 age5 field_51 field_50 field_52], + %w[age6_known details_known_6 age6 field_55 field_54 field_56], + ].each do |known, details_known, age, field, relationship, gender| + describe "##{known} and ##{age}" do + context "when #{field} is blank" do + context "and person details are blank" do + let(:attributes) { setup_section_params.merge({ field.to_s => nil, relationship.to_sym => nil, gender.to_sym => nil, field_15: "1", field_41: "5" }) } + + it "does not set ##{known}" do + unless known == "age1_known" + expect(parser.log.public_send(known)).to be_nil + end + end + + it "sets ##{details_known} to no" do + unless details_known == "details_known_1" + expect(parser.log.public_send(details_known)).to eq(2) + end + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end + + context "and person details are given" do + let(:attributes) { setup_section_params.merge({ field.to_sym => nil, relationship.to_sym => "C", gender.to_sym => "X", field_15: "1", field_41: "5" }) } + + it "does not set ##{age}" do + parser.valid? + expect(parser.errors[field.to_sym]).to include(/You must answer/) + end + end + end + + context "when #{field} is R" do + let(:attributes) { setup_section_params.merge({ field.to_s => "R", field_14: "1", field_41: "5", field_15: "1" }) } + + it "sets ##{known} 1" do + expect(parser.log.public_send(known)).to be(1) + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end + + context "when #{field} is a number" do + let(:attributes) { setup_section_params.merge({ field.to_s => "50", field_14: "1", field_41: "5", field_15: "1" }) } + + it "sets ##{known} to 0" do + expect(parser.log.public_send(known)).to be(0) + end + + it "sets ##{age} to given age" do + expect(parser.log.public_send(age)).to be(50) + end + end + + context "when #{field} is a non-sensical value" do + let(:attributes) { setup_section_params.merge({ field.to_s => "A", field_14: "1", field_41: "5", field_15: "1" }) } + + it "sets ##{known} to 0" do + expect(parser.log.public_send(known)).to be(0) + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end + end + end + + describe "field_39" do # ecstat2 + context "when buyer 2 has no age but has ecstat as child" do + let(:attributes) { valid_attributes.merge({ field_35: nil, field_39: "9" }) } + + it "a custom validation is applied" do + parser.valid? + + expect(parser.errors[:field_39]).to include I18n.t("validations.sales.2025.bulk_upload.ecstat2.buyer_cannot_be_child") + end + end + + context "when buyer 2 is under 16" do + let(:attributes) { valid_attributes.merge({ field_35: "9" }) } + + it "a custom validation is applied" do + parser.valid? + + validation_message = "Buyer 2’s age must be between 16 and 110." + expect(parser.errors[:field_35]).to include validation_message + end + end + + context "when buyer 2 is over 16 but has ecstat as child" do + let(:attributes) { valid_attributes.merge({ field_35: "17", field_39: "9" }) } + + it "a custom validation is applied" do + parser.valid? + + expect(parser.errors[:field_39]).to include I18n.t("validations.sales.2025.bulk_upload.ecstat2.buyer_cannot_be_over_16_and_child") + expect(parser.errors[:field_35]).to include I18n.t("validations.sales.2025.bulk_upload.age2.buyer_cannot_be_over_16_and_child") + end + end + + context "when person 2 a child but not a buyer" do + let(:attributes) { valid_attributes.merge({ field_12: 2, field_35: "10", field_39: "9" }) } + + it "does not add errors to their age and ecstat fields" do + parser.valid? + expect(parser.errors[:field_35]).to be_empty + expect(parser.errors[:field_39]).to be_empty + end + end + end + + describe "field_32" do # ecstat1 + context "when buyer 1 has no age but has ecstat as child" do + let(:attributes) { valid_attributes.merge({ field_28: nil, field_32: "9" }) } + + it "a custom validation is applied" do + parser.valid? + + expect(parser.errors[:field_32]).to include I18n.t("validations.sales.2025.bulk_upload.ecstat1.buyer_cannot_be_child") + end + end + + context "when buyer 1 is under 16" do + let(:attributes) { valid_attributes.merge({ field_28: "9" }) } + + it "a custom validation is applied" do + parser.valid? + + validation_message = "Buyer 1’s age must be between 16 and 110." + expect(parser.errors[:field_28]).to include validation_message + end + end + + context "when buyer 1 is over 16 but has ecstat as child" do + let(:attributes) { valid_attributes.merge({ field_28: "17", field_32: "9" }) } + + it "a custom validation is applied" do + parser.valid? + + expect(parser.errors[:field_32]).to include I18n.t("validations.sales.2025.bulk_upload.ecstat1.buyer_cannot_be_over_16_and_child") + expect(parser.errors[:field_28]).to include I18n.t("validations.sales.2025.bulk_upload.age1.buyer_cannot_be_over_16_and_child") + end + end + end + + describe "#field_33" do # will buyer1 live in property? + context "when not a possible value" do + let(:attributes) { valid_attributes.merge({ field_10: "2", field_33: "3" }) } + + it "is not valid" do + parser.valid? + expect(parser.errors).to include(:field_33) + end + end + end + + describe "#field_109" do # staircasing mortgageused + context "when invalid value" do + let(:attributes) { setup_section_params.merge(field_109: "4") } + + it "returns correct errors" do + parser.valid? + expect(parser.errors[:field_109]).to include(I18n.t("validations.sales.2025.bulk_upload.invalid_option", question: "was a mortgage used for this staircasing transaction?")) + + parser.log.blank_invalid_non_setup_fields! + parser.log.save! + expect(parser.log.mortgageused).to be_nil + end + end + + context "when value is 3 and stairowned is not 100" do + let(:attributes) { setup_section_params.merge(field_109: "3", field_10: "1", field_96: "50", field_97: "99", field_120: nil) } + + it "returns correct errors" do + parser.valid? + expect(parser.errors[:field_109]).to include("The percentage owned has to be 100% if the mortgage used is 'Don’t know'") + + parser.log.blank_invalid_non_setup_fields! + parser.log.save! + expect(parser.log.mortgageused).to be_nil + end + end + + context "when value is 3 and stairowned is not answered" do + let(:attributes) { setup_section_params.merge(field_109: "3", field_10: "1", field_96: "50", field_97: nil, field_120: nil) } + + it "does not add errors" do + parser.valid? + expect(parser.errors[:field_109]).to be_empty + end + end + + context "when value is 3 and stairowned is 100" do + let(:attributes) { setup_section_params.merge(field_109: "3", field_10: "1", field_96: "50", field_97: "100", field_120: nil) } + + it "does not add errors and sets mortgage used to 3" do + parser.valid? + expect(parser.log.mortgageused).to eq(3) + expect(parser.log.stairowned).to eq(100) + expect(parser.log.deposit).to be_nil + expect(parser.errors[:field_109]).to be_empty + expect(parser.errors[:field_120]).to be_empty + end + end + end + + describe "#field_88" do # shared ownership mortgageused + context "when invalid value" do + let(:attributes) { setup_section_params.merge(field_10: "2", field_88: "4") } + + it "returns correct errors" do + parser.valid? + expect(parser.errors[:field_88]).to include(I18n.t("validations.sales.2025.bulk_upload.invalid_option", question: "was a mortgage used for the purchase of this property? - Shared ownership.")) + + parser.log.blank_invalid_non_setup_fields! + parser.log.save! + expect(parser.log.mortgageused).to be_nil + end + end + + context "when value is 3 and stairowned is not answered" do + let(:attributes) { setup_section_params.merge(field_88: "3", field_10: "2", field_96: "50", field_97: nil, field_120: nil) } + + it "returns correct errors" do + parser.valid? + expect(parser.errors[:field_88]).to include(I18n.t("validations.invalid_option", question: "was a mortgage used for the purchase of this property?")) + + parser.log.blank_invalid_non_setup_fields! + parser.log.save! + expect(parser.log.mortgageused).to be_nil + end + end + + context "with non staircasing mortgage error" do + let(:attributes) { setup_section_params.merge(field_9: "30", field_88: "1", field_89: "10000", field_91: "5000", field_86: "30000", field_87: "28", field_10: "2") } + + it "does not add a BU error on type (because it's a setup field and would block log creation)" do + parser.valid? + expect(parser.errors[:field_9]).to be_empty + end + + it "includes errors on other related fields" do + parser.valid? + expect(parser.errors[:field_89]).to include("The mortgage (£10,000.00) and cash deposit (£5,000.00) added together is £15,000.00.

    The full purchase price (£30,000.00) multiplied by the percentage equity stake purchased (28.0%) is £8,400.00.

    These two amounts should be the same.") + expect(parser.errors[:field_91]).to include("The mortgage (£10,000.00) and cash deposit (£5,000.00) added together is £15,000.00.

    The full purchase price (£30,000.00) multiplied by the percentage equity stake purchased (28.0%) is £8,400.00.

    These two amounts should be the same.") + expect(parser.errors[:field_86]).to include("The mortgage (£10,000.00) and cash deposit (£5,000.00) added together is £15,000.00.

    The full purchase price (£30,000.00) multiplied by the percentage equity stake purchased (28.0%) is £8,400.00.

    These two amounts should be the same.") + expect(parser.errors[:field_87]).to include("The mortgage (£10,000.00) and cash deposit (£5,000.00) added together is £15,000.00.

    The full purchase price (£30,000.00) multiplied by the percentage equity stake purchased (28.0%) is £8,400.00.

    These two amounts should be the same.") + end + + it "does not add errors to other ownership type fields" do + parser.valid? + expect(parser.errors[:field_117]).to be_empty + expect(parser.errors[:field_120]).to be_empty + expect(parser.errors[:field_113]).to be_empty + expect(parser.errors[:field_107]).to be_empty + expect(parser.errors[:field_108]).to be_empty + expect(parser.errors[:field_116]).to be_empty + expect(parser.errors[:field_109]).to be_empty + end + end + end + + describe "#field_116" do + let(:attributes) { valid_attributes.merge({ field_8: "2", field_11: "9", field_116: "3" }) } + + it "does not allow 3 (don't know) as an option for discounted ownership" do + parser.valid? + expect(parser.errors[:field_116]).to include(I18n.t("validations.invalid_option", question: "was a mortgage used for the purchase of this property?")) + + parser.log.blank_invalid_non_setup_fields! + parser.log.save! + expect(parser.log.mortgageused).to be_nil + end + + context "when validate_discounted_ownership_value is triggered" do + let(:attributes) { setup_section_params.merge(field_113: 100, field_120: 100, field_8: 2, field_10: 2, field_11: 9, field_116: 2, field_115: 10) } + + it "only adds errors to the discounted ownership field" do + parser.valid? + expect(parser.errors[:field_88]).to be_empty + expect(parser.errors[:field_117]).to include("The mortgage and cash deposit (£100.00) added together is £100.00.

    The full purchase price (£100.00) subtracted by the sum of the full purchase price (£100.00) multiplied by the percentage discount (10.0%) is £90.00.

    These two amounts should be the same.") + expect(parser.errors[:field_126]).to be_empty + end + end + end + + describe "soft validations" do + context "when soft validation is triggered" do + let(:attributes) { valid_attributes.merge({ field_10: 2, field_28: 22, field_32: 5 }) } + + it "adds an error to the relevant fields" do + parser.valid? + expect(parser.errors.where(:field_28, category: :soft_validation)).to be_present + expect(parser.errors.where(:field_32, category: :soft_validation)).to be_present + end + + it "populates with correct error message" do + parser.valid? + expect(parser.errors.where(:field_28, category: :soft_validation).first.message).to eql("You told us this person is aged 22 years and retired. The minimum expected retirement age in England is 66.") + expect(parser.errors.where(:field_32, category: :soft_validation).first.message).to eql("You told us this person is aged 22 years and retired. The minimum expected retirement age in England is 66.") + end + end + end + + describe "log_already_exists?" do + let(:attributes) { { bulk_upload: } } + + before do + build(:sales_log, owning_organisation: nil, saledate: nil, purchid: nil, age1: nil, sex1: nil, ecstat1: nil).save(validate: false) + end + + it "does not add duplicate logs validation to the blank row" do + expect(parser.log_already_exists?).to eq(false) + end + end + end + + describe "#log" do + describe "#noint" do + context "when field is set to 1" do + let(:attributes) { valid_attributes.merge({ field_14: 1 }) } + + it "is correctly set" do + expect(parser.log.noint).to be(1) + end + end + + context "when field is set to 2" do + let(:attributes) { valid_attributes.merge({ field_14: 2 }) } + + it "is correctly set" do + expect(parser.log.noint).to be(2) + end + end + end + + describe "#uprn" do + let(:attributes) { setup_section_params.merge({ field_16: "12" }) } + + it "is correctly set" do + expect(parser.log.uprn).to eql("12") + end + end + + describe "#uprn_known" do + context "when uprn known" do + let(:attributes) { setup_section_params.merge({ field_16: "12" }) } + + it "is correctly set" do + expect(parser.log.uprn_known).to be(1) + end + end + + context "when uprn not known" do + let(:attributes) { setup_section_params.merge({ field_16: nil }) } + + it "is correctly set" do + expect(parser.log.uprn_known).to be(0) + end + end + end + + describe "#address_line1" do + let(:attributes) { setup_section_params.merge({ field_17: "some street" }) } + + it "is correctly set" do + expect(parser.log.address_line1).to eql("some street") + end + end + + describe "#address_line2" do + let(:attributes) { setup_section_params.merge({ field_18: "some other street" }) } + + it "is correctly set" do + expect(parser.log.address_line2).to eql("some other street") + end + end + + describe "#town_or_city" do + let(:attributes) { setup_section_params.merge({ field_19: "some town" }) } + + it "is correctly set" do + expect(parser.log.town_or_city).to eql("some town") + end + end + + describe "#county" do + let(:attributes) { setup_section_params.merge({ field_20: "some county" }) } + + it "is correctly set" do + expect(parser.log.county).to eql("some county") + end + end + + describe "#ethnic_group" do + context "when field_30 is 20" do + let(:attributes) { setup_section_params.merge({ field_30: "20" }) } + + it "is correctly set" do + expect(parser.log.ethnic_group).to be(0) + end + end + end + + describe "#ethnic_group2" do + let(:attributes) { setup_section_params.merge({ field_37: "1" }) } + + it "is correctly set" do + expect(parser.log.ethnic_group2).to be(0) + end + + context "when field_37 is 20" do + let(:attributes) { setup_section_params.merge({ field_37: "20" }) } + + it "is correctly set" do + expect(parser.log.ethnic_group2).to be(0) + end + end + end + + describe "#ethnicbuy2" do + let(:attributes) { setup_section_params.merge({ field_37: "1" }) } + + it "is correctly set" do + expect(parser.log.ethnicbuy2).to be(1) + end + end + + describe "#nationality_all" do + context "when field_31 is a 3 digit nationality code" do + let(:attributes) { setup_section_params.merge({ field_31: "036" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(36) + expect(parser.log.nationality_all_group).to be(12) + end + end + + context "when field_31 is a nationality code without the trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_31: "36" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(36) + expect(parser.log.nationality_all_group).to be(12) + end + end + + context "when field_31 is a nationality code with trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_31: "0036" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(36) + expect(parser.log.nationality_all_group).to be(12) + end + end + + context "when field_31 is 0" do + let(:attributes) { setup_section_params.merge({ field_31: "0" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(0) + expect(parser.log.nationality_all_group).to be(0) + end + end + + context "when field_31 is 000" do + let(:attributes) { setup_section_params.merge({ field_31: "000" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(0) + expect(parser.log.nationality_all_group).to be(0) + end + end + + context "when field_31 is 0000" do + let(:attributes) { setup_section_params.merge({ field_31: "0000" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(0) + expect(parser.log.nationality_all_group).to be(0) + end + end + + context "when field_31 is 826" do + let(:attributes) { setup_section_params.merge({ field_31: "826" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(826) + expect(parser.log.nationality_all_group).to be(826) + end + end + + context "when field_31 is 826 with trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_31: "0826" }) } + + it "is correctly set" do + expect(parser.log.nationality_all).to be(826) + expect(parser.log.nationality_all_group).to be(826) + end + end + + context "when field_31 is not a valid option" do + let(:attributes) { setup_section_params.merge({ field_31: "123123" }) } + + it "is correctly set" do + parser.valid? + expect(parser.log.nationality_all).to be(nil) + expect(parser.log.nationality_all_group).to be(nil) + expect(parser.errors["field_31"]).to include(I18n.t("validations.sales.2025.bulk_upload.nationality.invalid")) + end + end + end + + describe "#nationality_all_buyer2" do + context "when field_38 is a 3 digit nationality code" do + let(:attributes) { setup_section_params.merge({ field_38: "036" }) } + + it "is correctly set" do + expect(parser.log.nationality_all_buyer2).to be(36) + expect(parser.log.nationality_all_buyer2_group).to be(12) + end + end + + context "when field_38 is a nationality code without the trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_38: "36" }) } + + it "is correctly set" do + expect(parser.log.nationality_all_buyer2).to be(36) + expect(parser.log.nationality_all_buyer2_group).to be(12) + end + end + + context "when field_38 is a nationality code with trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_38: "0036" }) } + + it "is correctly set" do + expect(parser.log.nationality_all_buyer2).to be(36) + expect(parser.log.nationality_all_buyer2_group).to be(12) + end + end + + context "when field_38 is 0" do + let(:attributes) { setup_section_params.merge({ field_38: "0" }) } + + it "is correctly set" do + expect(parser.log.nationality_all_buyer2).to be(0) + expect(parser.log.nationality_all_buyer2_group).to be(0) + end + end + + context "when field_38 is 000" do + let(:attributes) { setup_section_params.merge({ field_38: "000" }) } + + it "is correctly set" do + expect(parser.log.nationality_all_buyer2).to be(0) + expect(parser.log.nationality_all_buyer2_group).to be(0) + end + end + + context "when field_38 is 0000" do + let(:attributes) { setup_section_params.merge({ field_38: "0000" }) } + + it "is correctly set" do + expect(parser.log.nationality_all_buyer2).to be(0) + expect(parser.log.nationality_all_buyer2_group).to be(0) + end + end + + context "when field_38 is 826" do + let(:attributes) { setup_section_params.merge({ field_38: "826" }) } + + it "is correctly set" do + expect(parser.log.nationality_all_buyer2).to be(826) + expect(parser.log.nationality_all_buyer2_group).to be(826) + end + end + + context "when field_38 is 826 with trailing 0s" do + let(:attributes) { setup_section_params.merge({ field_38: "0826" }) } + + it "is correctly set" do + expect(parser.log.nationality_all_buyer2).to be(826) + expect(parser.log.nationality_all_buyer2_group).to be(826) + end + end + + context "when field_38 is not a valid option" do + let(:attributes) { setup_section_params.merge({ field_38: "123123" }) } + + it "is correctly set" do + parser.valid? + expect(parser.log.nationality_all_buyer2).to be(nil) + expect(parser.log.nationality_all_buyer2_group).to be(nil) + expect(parser.errors["field_38"]).to include(I18n.t("validations.sales.2025.bulk_upload.nationality.invalid")) + end + end + end + + describe "#buy2living" do + let(:attributes) { setup_section_params.merge({ field_63: "1" }) } + + it "is correctly set" do + expect(parser.log.buy2living).to be(1) + end + end + + describe "#prevtenbuy2" do + let(:attributes) { setup_section_params.merge({ field_64: "R" }) } + + it "is correctly set" do + expect(parser.log.prevtenbuy2).to be(0) + end + end + + describe "#hhregres" do + let(:attributes) { setup_section_params.merge({ field_65: "1" }) } + + it "is correctly set" do + expect(parser.log.hhregres).to be(1) + end + end + + describe "#hhregresstill" do + let(:attributes) { setup_section_params.merge({ field_66: "4" }) } + + it "is correctly set" do + expect(parser.log.hhregresstill).to be(4) + end + end + + describe "#prevshared" do + let(:attributes) { setup_section_params.merge({ field_77: "3" }) } + + it "is correctly set" do + expect(parser.log.prevshared).to be(3) + end + end + + describe "#staircasesale" do + let(:attributes) { setup_section_params.merge({ field_98: "1" }) } + + it "is correctly set" do + expect(parser.log.staircasesale).to be(1) + end + end + + describe "#soctenant" do + context "when discounted ownership" do + let(:attributes) { valid_attributes.merge({ field_8: "2" }) } + + it "is set to nil" do + expect(parser.log.soctenant).to be_nil + end + end + + context "when shared ownership" do + context "when prevten is a social housing type" do + let(:attributes) { valid_attributes.merge({ field_8: "1", field_58: "1" }) } + + it "is set to yes" do + expect(parser.log.soctenant).to be(1) + end + end + + context "when prevten is not a social housing type" do + context "and prevtenbuy2 is a social housing type" do + let(:attributes) { valid_attributes.merge({ field_8: "1", field_58: "3", field_64: "2" }) } + + it "is set to yes" do + expect(parser.log.soctenant).to be(1) + end + end + + context "and prevtenbuy2 is not a social housing type" do + let(:attributes) { valid_attributes.merge({ field_8: "1", field_58: "3", field_64: "4" }) } + + it "is set to no" do + expect(parser.log.soctenant).to be(2) + end + end + + context "and prevtenbuy2 is blank" do + let(:attributes) { valid_attributes.merge({ field_8: "1", field_58: "3", field_64: nil }) } + + it "is set to no" do + expect(parser.log.soctenant).to be(2) + end + end + end + end + end + + describe "with living before purchase years for shared ownership more than 0" do + let(:attributes) { setup_section_params.merge({ field_8: "1", field_79: "1" }) } + + it "is sets living before purchase asked to yes and sets the correct living before purchase years" do + expect(parser.log.proplen_asked).to be(0) + expect(parser.log.proplen).to be(1) + end + end + + describe "with living before purchase years for discounted ownership more than 0" do + let(:attributes) { setup_section_params.merge({ field_8: "2", field_112: "1" }) } + + it "is sets living before purchase asked to yes and sets the correct living before purchase years" do + expect(parser.log.proplen_asked).to be(0) + expect(parser.log.proplen).to be(1) + end + end + + describe "with living before purchase years for shared ownership set to 0" do + let(:attributes) { setup_section_params.merge({ field_8: "1", field_79: "0" }) } + + it "is sets living before purchase asked to no" do + expect(parser.log.proplen_asked).to be(1) + expect(parser.log.proplen).to be_nil + end + end + + describe "with living before purchase 0 years for discounted ownership set to 0" do + let(:attributes) { setup_section_params.merge({ field_8: "2", field_112: "0" }) } + + it "is sets living before purchase asked to no" do + expect(parser.log.proplen_asked).to be(1) + expect(parser.log.proplen).to be_nil + end + end + + context "when mscharge is given, but is set to 0 for shared ownership" do + let(:attributes) { valid_attributes.merge(field_94: "0") } + + it "does not override variables correctly" do + log = parser.log + expect(log["has_mscharge"]).to eq(0) # no + expect(log["mscharge"]).to be_nil + end + end + + context "when mscharge is given, but is set to 0 for discounted ownership" do + let(:attributes) { valid_attributes.merge(field_8: "2", field_121: "0") } + + it "does not override variables correctly" do + log = parser.log + expect(log["has_mscharge"]).to eq(0) # no + expect(log["mscharge"]).to be_nil + end + end + + describe "shared ownership sale type" do + context "when 32 is selected for shared ownership type" do + let(:attributes) { valid_attributes.merge(field_9: "32") } + + it "sets the value correctly" do + log = parser.log + expect(log.type).to eq(32) + end + end + end + + describe "#managing_organisation_id" do + let(:attributes) { setup_section_params } + + context "when user is part of the owning organisation" do + it "sets managing organisation to the correct organisation" do + parser.valid? + expect(parser.log.owning_organisation_id).to be(owning_org.id) + expect(parser.log.managing_organisation_id).to be(managing_org.id) + end + end + + context "when blank" do + let(:attributes) { { bulk_upload:, field_5: "", field_3: "not blank" } } + + it "is not permitted as setup error" do + parser.valid? + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_5 }.message).to eql(I18n.t("validations.not_answered", question: "reported by.")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when cannot find managing org" do + let(:attributes) { { bulk_upload:, field_5: "donotexist" } } + + it "is not permitted as setup error" do + parser.valid? + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_5 }.message).to eql(I18n.t("validations.not_answered", question: "reported by.")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + + context "when not affiliated with managing org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_4: owning_org.old_visible_id, field_5: unaffiliated_org.old_visible_id } } + + it "is not permitted as setup error" do + parser.valid? + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_5 }.message).to eql(I18n.t("validations.sales.2025.bulk_upload.assigned_to.managing_organisation_not_related")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + end + end + + describe "#owning_organisation_id" do + let(:attributes) { setup_section_params } + + context "when owning organisation does not own stock" do + let(:owning_org) { create(:organisation, :with_old_visible_id, holds_own_stock: false) } + let(:attributes) { setup_section_params } + + it "is not permitted as setup error" do + parser.valid? + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_4 }.message).to eql(I18n.t("validations.sales.2025.bulk_upload.owning_organisation.not_stock_owner")) + end + + it "blocks log creation" do + parser.valid? + expect(parser).to be_block_log_creation + end + end + end + + describe "#spreadsheet_duplicate_hash" do + it "returns a hash" do + expect(parser.spreadsheet_duplicate_hash).to be_a(Hash) + end + end + + describe "#add_duplicate_found_in_spreadsheet_errors" do + it "adds errors" do + expect { parser.add_duplicate_found_in_spreadsheet_errors }.to change(parser.errors, :size) + end + end +end diff --git a/spec/services/csv/sales_log_csv_service_spec.rb b/spec/services/csv/sales_log_csv_service_spec.rb index 3cf56af2e..5e041b2d5 100644 --- a/spec/services/csv/sales_log_csv_service_spec.rb +++ b/spec/services/csv/sales_log_csv_service_spec.rb @@ -199,7 +199,7 @@ RSpec.describe Csv::SalesLogCsvService do let(:fixed_time) { Time.zone.local(2024, 5, 1) } before do - log.update!(nationality_all: 36) + log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1) end it "exports the CSV with the 2024 ordering and all values correct" do @@ -286,6 +286,10 @@ RSpec.describe Csv::SalesLogCsvService do let(:fixed_time) { Time.zone.local(2024, 5, 1) } let(:year) { 2024 } + before do + log.update!(manual_address_entry_selected: false, uprn: "1", uprn_known: 1) + end + it "exports the CSV with all values correct" do expected_content = CSV.read("spec/fixtures/files/sales_logs_csv_export_codes_24.csv") values_to_delete = %w[ID] @@ -338,7 +342,7 @@ RSpec.describe Csv::SalesLogCsvService do let(:fixed_time) { Time.zone.local(2024, 5, 1) } before do - log.update!(nationality_all: 36) + log.update!(nationality_all: 36, manual_address_entry_selected: false, uprn: "1", uprn_known: 1) end context "and exporting with labels" do diff --git a/spec/services/exports/lettings_log_export_service_spec.rb b/spec/services/exports/lettings_log_export_service_spec.rb index 1db9cf4d9..12e3809f0 100644 --- a/spec/services/exports/lettings_log_export_service_spec.rb +++ b/spec/services/exports/lettings_log_export_service_spec.rb @@ -431,7 +431,7 @@ RSpec.describe Exports::LettingsLogExportService do end context "and one lettings log is available for export" do - let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, age1: 35, sex1: "F", age2: 32, sex2: "M", ppostcode_full: "A1 1AA", nationality_all_group: 13, propcode: "123", postcode_full: "SE2 6RT", tenancycode: "BZ737", startdate: Time.zone.local(2024, 4, 2, 10, 36, 49), voiddate: Time.zone.local(2021, 11, 3), mrcdate: Time.zone.local(2022, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4, creation_method: 2, bulk_upload_id: 1, address_line1_as_entered: "address line 1 as entered", address_line2_as_entered: "address line 2 as entered", town_or_city_as_entered: "town or city as entered", county_as_entered: "county as entered", postcode_full_as_entered: "AB1 2CD", la_as_entered: "la as entered") } + let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, assigned_to: user, age1: 35, sex1: "F", age2: 32, sex2: "M", ppostcode_full: "A1 1AA", nationality_all_group: 13, propcode: "123", postcode_full: "SE2 6RT", tenancycode: "BZ737", startdate: Time.zone.local(2024, 4, 2, 10, 36, 49), voiddate: Time.zone.local(2021, 11, 3), mrcdate: Time.zone.local(2022, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4, creation_method: 2, bulk_upload_id: 1, address_line1_as_entered: "address line 1 as entered", address_line2_as_entered: "address line 2 as entered", town_or_city_as_entered: "town or city as entered", county_as_entered: "county as entered", postcode_full_as_entered: "AB1 2CD", la_as_entered: "la as entered", manual_address_entry_selected: false, uprn: "1", uprn_known: 1) } let(:expected_zip_filename) { "core_2024_2025_apr_mar_f0001_inc0001.zip" } let(:expected_data_filename) { "core_2024_2025_apr_mar_f0001_inc0001_pt001.xml" } let(:xml_export_file) { File.open("spec/fixtures/exports/general_needs_log_24_25.xml", "r:UTF-8") } diff --git a/spec/shared/shared_examples_for_derived_fields.rb b/spec/shared/shared_examples_for_derived_fields.rb index 3e6b685df..3f746468a 100644 --- a/spec/shared/shared_examples_for_derived_fields.rb +++ b/spec/shared/shared_examples_for_derived_fields.rb @@ -25,11 +25,14 @@ RSpec.shared_examples "shared examples for derived fields" do |log_type| end it "does not affect older logs with uprn_confirmed == 0" do - log = FactoryBot.build(log_type, uprn_known: 0, uprn: nil, uprn_confirmed: 0) - - expect { log.set_derived_fields! }.to not_change(log, :uprn_known) - .and not_change(log, :uprn) - .and not_change(log, :uprn_confirmed) + Timecop.freeze(Time.zone.local(2023, 4, 1)) do + log = FactoryBot.build(log_type, uprn_known: 0, uprn: nil, uprn_confirmed: 0) + allow(log.form).to receive(:start_year_2024_or_later?).and_return(false) + expect { log.set_derived_fields! }.to not_change(log, :uprn_known) + .and not_change(log, :uprn) + .and not_change(log, :uprn_confirmed) + end + Timecop.return end end end diff --git a/spec/shared/shared_log_examples.rb b/spec/shared/shared_log_examples.rb index ef9ba32e6..77be654d7 100644 --- a/spec/shared/shared_log_examples.rb +++ b/spec/shared/shared_log_examples.rb @@ -81,7 +81,6 @@ RSpec.shared_examples "shared log examples" do |log_type| expect { log.process_uprn_change! }.to change(log, :address_line1).from(nil).to("0, Building Name, Thoroughfare") .and change(log, :town_or_city).from(nil).to("Posttown") .and change(log, :postcode_full).from(nil).to("POSTCODE") - .and change(log, :uprn_confirmed).from(1).to(nil) .and change(log, :county).from("county").to(nil) end end @@ -156,7 +155,6 @@ RSpec.shared_examples "shared log examples" do |log_type| .and change(log, :uprn_confirmed).from(nil).to(1) .and change(log, :uprn).from(nil).to("UPRN") .and change(log, :uprn_known).from(nil).to(1) - .and change(log, :uprn_selection).from("UPRN").to(nil) .and change(log, :county).from("county").to(nil) end end