From 1520767dd111bdc3f7091bacb50af61153abcfe9 Mon Sep 17 00:00:00 2001 From: Oscar Richardson <116292912+oscar-richardson-softwire@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:11:25 +0100 Subject: [PATCH] CLDC-4461: Hide confidential addresses (#3371) * Hide address questions for schemes marked 'sensitive' * Add BU handling * Add redundant routing to LA question logic * Small refactors * Remove redundant comment * Add supported housing only error message to Q16 * Add supported housing only error message to Q17 * Add the drop-down on Q16 * Add suffix to BU errors * Fix failing test * Improve tests * Small refactors * Sensitive test value refactor * Fix lint --- app/controllers/form_controller.rb | 2 +- .../lettings_log_variables.rb | 7 ++ .../form/lettings/questions/address_line1.rb | 6 + .../form/lettings/questions/address_search.rb | 6 + .../form/lettings/questions/declaration.rb | 2 +- app/models/form/lettings/questions/uprn.rb | 2 +- .../form/lettings/questions/uprn_known.rb | 2 +- app/models/form/question.rb | 2 +- .../form/sales/questions/privacy_notice.rb | 2 +- app/models/form/sales/questions/uprn.rb | 2 +- app/models/form/sales/questions/uprn_known.rb | 2 +- app/models/lettings_log.rb | 8 +- app/models/scheme.rb | 4 + .../lettings/year2026/row_parser.rb | 66 ++++++----- .../form/guidance/_address_search.html.erb | 6 + .../forms/2026/lettings/guidance.en.yml | 4 + .../lettings/2026/bulk_upload.en.yml | 1 + .../lettings/property_information.en.yml | 4 + .../pages/property_local_authority_spec.rb | 67 ++++++++--- .../lettings/questions/address_line1_spec.rb | 30 +++++ .../lettings/questions/address_search_spec.rb | 30 +++++ .../lettings_log_derived_fields_spec.rb | 107 ++++++++++++++++++ spec/models/lettings_log_spec.rb | 19 ++++ .../form/address_search_guidance_spec.rb | 29 +++++ .../lettings/year2026/row_parser_spec.rb | 100 ++++++++++++++-- 25 files changed, 446 insertions(+), 64 deletions(-) create mode 100644 spec/requests/form/address_search_guidance_spec.rb diff --git a/app/controllers/form_controller.rb b/app/controllers/form_controller.rb index 6dba53a8a..469151236 100644 --- a/app/controllers/form_controller.rb +++ b/app/controllers/form_controller.rb @@ -35,7 +35,7 @@ class FormController < ApplicationController else @log.valid? if mandatory_questions_with_no_response.any? mandatory_questions_with_no_response.map do |question| - @log.errors.add question.id.to_sym, question.unanswered_error_message, category: :not_answered + @log.errors.add question.id.to_sym, question.unanswered_error_message(@log), category: :not_answered end error_attributes = @log.errors.map(&:attribute) Rails.logger.info "User triggered validation(s) on: #{error_attributes.join(', ')}" diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index f0f4ff279..4b6ed27af 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -181,6 +181,13 @@ module DerivedVariables::LettingsLogVariables self.la = nil end + if scheme_has_confidential_information? + reset_address_fields! + self.uprn_selection = nil + self.postcode_known = nil + self.manual_address_entry_selected = nil + end + clear_gender_description_unless_gender_not_same_as_sex! if form.start_year_2026_or_later? set_checkbox_values! diff --git a/app/models/form/lettings/questions/address_line1.rb b/app/models/form/lettings/questions/address_line1.rb index 2f88b210e..c96e8db1e 100644 --- a/app/models/form/lettings/questions/address_line1.rb +++ b/app/models/form/lettings/questions/address_line1.rb @@ -18,5 +18,11 @@ class Form::Lettings::Questions::AddressLine1 < ::Form::Question ].select(&:present?).join("\n") end + def unanswered_error_message(log = nil) + return super unless log&.is_supported_housing? + + I18n.t("validations.lettings.property.address_line1.not_answered_supported_housing") + end + QUESTION_NUMBER_FROM_YEAR = { 2023 => 12, 2024 => 13, 2025 => 17, 2026 => 17 }.freeze end diff --git a/app/models/form/lettings/questions/address_search.rb b/app/models/form/lettings/questions/address_search.rb index eb970a5e5..5955abc0a 100644 --- a/app/models/form/lettings/questions/address_search.rb +++ b/app/models/form/lettings/questions/address_search.rb @@ -38,5 +38,11 @@ class Form::Lettings::Questions::AddressSearch < ::Form::Question answer_options(log, user).transform_values { |value| value["value"] } || {} end + def unanswered_error_message(log = nil) + return super unless log&.is_supported_housing? + + I18n.t("validations.lettings.property.address.not_answered_supported_housing") + end + QUESTION_NUMBER_FROM_YEAR = { 2024 => 12, 2025 => 16, 2026 => 16 }.freeze end diff --git a/app/models/form/lettings/questions/declaration.rb b/app/models/form/lettings/questions/declaration.rb index 6a2572487..7c5c7ebdb 100644 --- a/app/models/form/lettings/questions/declaration.rb +++ b/app/models/form/lettings/questions/declaration.rb @@ -18,7 +18,7 @@ class Form::Lettings::Questions::Declaration < ::Form::Question { "declaration" => { "value" => declaration_text } }.freeze end - def unanswered_error_message + def unanswered_error_message(_log = nil) if form.start_year_2024_or_later? I18n.t("validations.declaration.missing.post_2024") else diff --git a/app/models/form/lettings/questions/uprn.rb b/app/models/form/lettings/questions/uprn.rb index 98ffb75c2..f95a02f7e 100644 --- a/app/models/form/lettings/questions/uprn.rb +++ b/app/models/form/lettings/questions/uprn.rb @@ -14,7 +14,7 @@ class Form::Lettings::Questions::Uprn < ::Form::Question ] end - def unanswered_error_message + def unanswered_error_message(_log = nil) I18n.t("validations.lettings.property.uprn.invalid") end diff --git a/app/models/form/lettings/questions/uprn_known.rb b/app/models/form/lettings/questions/uprn_known.rb index 5dab0f35a..dfc79d3ae 100644 --- a/app/models/form/lettings/questions/uprn_known.rb +++ b/app/models/form/lettings/questions/uprn_known.rb @@ -26,7 +26,7 @@ class Form::Lettings::Questions::UprnKnown < ::Form::Question "0" => { "value" => "No" }, }.freeze - def unanswered_error_message + def unanswered_error_message(_log = nil) I18n.t("validations.property.uprn_known.invalid") end diff --git a/app/models/form/question.rb b/app/models/form/question.rb index 9b38cf511..edbbaf3f3 100644 --- a/app/models/form/question.rb +++ b/app/models/form/question.rb @@ -240,7 +240,7 @@ class Form::Question format_ending(label) end - def unanswered_error_message + def unanswered_error_message(_log = nil) question_text = error_display_label.presence || "this question." I18n.t("validations.not_answered", question: question_text.downcase) end diff --git a/app/models/form/sales/questions/privacy_notice.rb b/app/models/form/sales/questions/privacy_notice.rb index aac52baca..be949f7d3 100644 --- a/app/models/form/sales/questions/privacy_notice.rb +++ b/app/models/form/sales/questions/privacy_notice.rb @@ -19,7 +19,7 @@ class Form::Sales::Questions::PrivacyNotice < ::Form::Question { "privacynotice" => { "value" => declaration_text } }.freeze end - def unanswered_error_message + def unanswered_error_message(_log = nil) buyer_or_buyers = @joint_purchase ? "buyers" : "buyer" if form.start_year_2024_or_later? I18n.t("validations.privacynotice.missing.post_2024", buyer_or_buyers:) diff --git a/app/models/form/sales/questions/uprn.rb b/app/models/form/sales/questions/uprn.rb index 841818d58..f6e1b2ae2 100644 --- a/app/models/form/sales/questions/uprn.rb +++ b/app/models/form/sales/questions/uprn.rb @@ -14,7 +14,7 @@ class Form::Sales::Questions::Uprn < ::Form::Question ] end - def unanswered_error_message + def unanswered_error_message(_log = nil) I18n.t("validations.sales.property_information.uprn.invalid") end diff --git a/app/models/form/sales/questions/uprn_known.rb b/app/models/form/sales/questions/uprn_known.rb index af84c09f9..57a6d9c3c 100644 --- a/app/models/form/sales/questions/uprn_known.rb +++ b/app/models/form/sales/questions/uprn_known.rb @@ -26,7 +26,7 @@ class Form::Sales::Questions::UprnKnown < ::Form::Question "0" => { "value" => "No" }, }.freeze - def unanswered_error_message + def unanswered_error_message(_log = nil) I18n.t("validations.sales.property_information.uprn_known.invalid") end diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 5a11da38a..1eb9078ff 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -822,7 +822,13 @@ class LettingsLog < Log end def is_address_asked? - form.start_year_2026_or_later? || !is_supported_housing? + return false if scheme_has_confidential_information? + + form.start_year_2026_or_later? || is_general_needs? + end + + def scheme_has_confidential_information? + !!scheme&.has_confidential_information? end def referral_is_from_local_authority_housing_register? diff --git a/app/models/scheme.rb b/app/models/scheme.rb index 0aa481a1e..00d0a3cb8 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -367,6 +367,10 @@ class Scheme < ApplicationRecord status_at(6.months.from_now) == :deactivating_soon end + def has_confidential_information? + sensitive == "Yes" + end + def discard! update!(discarded_at: Time.zone.now) locations.each(&:discard!) diff --git a/app/services/bulk_upload/lettings/year2026/row_parser.rb b/app/services/bulk_upload/lettings/year2026/row_parser.rb index 13baad405..ed9745139 100644 --- a/app/services/bulk_upload/lettings/year2026/row_parser.rb +++ b/app/services/bulk_upload/lettings/year2026/row_parser.rb @@ -493,8 +493,8 @@ class BulkUpload::Lettings::Year2026::RowParser validate :validate_assigned_to_when_support, on: :after_log validate :validate_all_charges_given, 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_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log, unless: :scheme_has_confidential_information? + validate :validate_address_fields, on: :after_log, unless: :scheme_has_confidential_information? validate :validate_nationality, on: :after_log validate :validate_reasonpref_reason_values, on: :after_log @@ -688,9 +688,9 @@ private 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? + errors.add(field, with_confidential_scheme_suffix(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.")) + errors.add(:field_18, with_confidential_scheme_suffix(I18n.t("#{ERROR_BASE_KEY}.address.not_answered", question: "UPRN."))) end end @@ -701,23 +701,27 @@ private 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.")) + errors.add(:field_19, with_confidential_scheme_suffix(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.")) + errors.add(:field_21, with_confidential_scheme_suffix(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.")) + errors.add(:field_23, with_confidential_scheme_suffix(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.")) + errors.add(:field_24, with_confidential_scheme_suffix(I18n.t("#{ERROR_BASE_KEY}.not_answered", question: "part 2 of postcode."))) end end end + def with_confidential_scheme_suffix(message) + "#{message} #{I18n.t("#{ERROR_BASE_KEY}.address.confidential_scheme_suffix")}" + 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| @@ -1491,26 +1495,28 @@ private attributes["first_time_property_let_as_social_housing"] = first_time_property_let_as_social_housing - 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? + unless scheme_has_confidential_information? + 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? + end attributes["gender_same_as_sex1"] = field_43 attributes["gender_description1"] = field_44 @@ -1577,6 +1583,10 @@ private @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 scheme_has_confidential_information? + !!scheme&.has_confidential_information? + end + def location return if scheme.nil? diff --git a/app/views/form/guidance/_address_search.html.erb b/app/views/form/guidance/_address_search.html.erb index e93fb78b6..0d890c1ee 100644 --- a/app/views/form/guidance/_address_search.html.erb +++ b/app/views/form/guidance/_address_search.html.erb @@ -6,6 +6,12 @@ <%= I18n.t("forms.#{@log.form.start_date.year}.#{@log.form.type}.guidance.address_uprn.content").html_safe %> <% end %> +<% if @log.form.type == "lettings" && @log.form.start_year_2026_or_later? %> + <%= govuk_details(summary_text: I18n.t("forms.#{@log.form.start_date.year}.#{@log.form.type}.guidance.confidential_supported_lettings.title")) do %> + <%= I18n.t("forms.#{@log.form.start_date.year}.#{@log.form.type}.guidance.confidential_supported_lettings.content").html_safe %> + <% end %> +<% 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/config/locales/forms/2026/lettings/guidance.en.yml b/config/locales/forms/2026/lettings/guidance.en.yml index 3f2e315cf..7ea71158c 100644 --- a/config/locales/forms/2026/lettings/guidance.en.yml +++ b/config/locales/forms/2026/lettings/guidance.en.yml @@ -73,6 +73,10 @@ en: content: "

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 is 0010457355.

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

" + confidential_supported_lettings: + title: "What should I do for confidential supported lettings?" + content: "Full address or UPRN is not required for confidential supported lettings. You are seeing this question because the scheme you chose in the ‘Set up this lettings log’ section does not have the ‘Confidential information’ box ticked. Please check the scheme you chose. If a scheme needs updating to mark it as confidential, a CORE coordinator in your organisation can do this. If you or they need further assistance contact the helpdesk." + needs_type: title: "What does each need type mean?" content: "General needs housing includes both self-contained and shared housing without support or specific adaptations.

Supported housing is housing with special design facilities or features targeted at a specific client group requiring support, for example housing designed for older people, sheltered accommodation, extra care housing. It can include direct access hostels, group homes, and purpose-built self-contained housing. We do not require CORE logs for residential care or nursing homes." diff --git a/config/locales/validations/lettings/2026/bulk_upload.en.yml b/config/locales/validations/lettings/2026/bulk_upload.en.yml index 1206b7ad6..52bb3fa77 100644 --- a/config/locales/validations/lettings/2026/bulk_upload.en.yml +++ b/config/locales/validations/lettings/2026/bulk_upload.en.yml @@ -53,6 +53,7 @@ en: 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." + confidential_scheme_suffix: "If your letting is in a confidential scheme, check the scheme you chose. A coordinator can edit a scheme to confidential on the ‘Schemes' page." nationality: invalid: "Select a valid nationality." charges: diff --git a/config/locales/validations/lettings/property_information.en.yml b/config/locales/validations/lettings/property_information.en.yml index 15ea86f0e..5de2ad613 100644 --- a/config/locales/validations/lettings/property_information.en.yml +++ b/config/locales/validations/lettings/property_information.en.yml @@ -2,6 +2,10 @@ en: validations: lettings: property: + address: + not_answered_supported_housing: "You must enter address. If your letting is in a confidential scheme, please check the scheme you chose in the ‘Set up this lettings log’ section. If a scheme needs updating to mark it as confidential, a CORE coordinator in your organisation can do this." + address_line1: + not_answered_supported_housing: "You must enter address line 1. If your letting is in a confidential scheme, please check the scheme you chose in the ‘Set up this lettings log’ section. If a scheme needs updating to mark it as confidential, a CORE coordinator in your organisation can do this." postcode_full: invalid: "Enter a postcode in the correct format, for example AA1 1AA." not_in_england: "It looks like you have an entered a postcode outside of England. Only create logs for lettings in England." diff --git a/spec/models/form/lettings/pages/property_local_authority_spec.rb b/spec/models/form/lettings/pages/property_local_authority_spec.rb index 5e29424e3..c2b09dab8 100644 --- a/spec/models/form/lettings/pages/property_local_authority_spec.rb +++ b/spec/models/form/lettings/pages/property_local_authority_spec.rb @@ -33,28 +33,65 @@ RSpec.describe Form::Lettings::Pages::PropertyLocalAuthority, type: :model do end context "when routing to the page" do - let(:log) { build(:lettings_log) } - before do allow(form).to receive(:start_year_2025_or_later?).and_return(true) end - it "is routed to when la is not inferred and it is general needs log" do - log.needstype = 1 - log.is_la_inferred = false - expect(page).to be_routed_to(log, nil) - end + context "when the log is general needs" do + let(:log) { build(:lettings_log, needstype: 1) } + + it "is not routed to when `is_la_inferred` is nil" do + log.is_la_inferred = nil + expect(page).not_to be_routed_to(log, nil) + end - it "is not routed to when la is inferred" do - log.needstype = 1 - log.is_la_inferred = true - expect(page).not_to be_routed_to(log, nil) + it "is not routed to when LA is inferred" do + log.is_la_inferred = true + expect(page).not_to be_routed_to(log, nil) + end + + it "is routed to when LA is not inferred" do + log.is_la_inferred = false + expect(page).to be_routed_to(log, nil) + end end - it "is not routed to when it's a supported housing log" do - log.needstype = 2 - log.is_la_inferred = false - expect(page).not_to be_routed_to(log, nil) + context "when the log is supported housing" do + let(:log) { build(:lettings_log, needstype: 2) } + + it "is not routed to when `is_la_inferred` is nil" do + log.is_la_inferred = nil + expect(page).not_to be_routed_to(log, nil) + end + + it "is not routed to when LA is inferred" do + log.is_la_inferred = true + expect(page).not_to be_routed_to(log, nil) + end + + it "is not routed to, even when LA is not inferred" do + log.is_la_inferred = false + expect(page).not_to be_routed_to(log, nil) + end + + context "when the scheme has confidential information" do + let(:log) { build(:lettings_log, needstype: 2, scheme: build(:scheme, sensitive: 1)) } + + it "is not routed to when `is_la_inferred` is nil" do + log.is_la_inferred = nil + expect(page).not_to be_routed_to(log, nil) + end + + it "is not routed to when LA is inferred" do + log.is_la_inferred = true + expect(page).not_to be_routed_to(log, nil) + end + + it "is not routed to, even when LA is not inferred" do + log.is_la_inferred = false + expect(page).not_to be_routed_to(log, nil) + end + end end end end diff --git a/spec/models/form/lettings/questions/address_line1_spec.rb b/spec/models/form/lettings/questions/address_line1_spec.rb index 212e3be9b..ce4903c0d 100644 --- a/spec/models/form/lettings/questions/address_line1_spec.rb +++ b/spec/models/form/lettings/questions/address_line1_spec.rb @@ -38,4 +38,34 @@ RSpec.describe Form::Lettings::Questions::AddressLine1, type: :model do it "has the correct check_answers_card_number" do expect(question.check_answers_card_number).to be_nil end + + describe "#unanswered_error_message" do + context "when the log is supported housing" do + let(:log) { build(:lettings_log, needstype: 2) } + + it "returns the confidential-scheme guidance message" do + expect(question.unanswered_error_message(log)).to eq( + "You must enter address line 1. If your letting is in a confidential scheme, please check the scheme you chose in the ‘Set up this lettings log’ section. If a scheme needs updating to mark it as confidential, a CORE coordinator in your organisation can do this.", + ) + end + end + + context "when the log is general needs" do + let(:log) { build(:lettings_log, needstype: 1) } + + it "returns the default unanswered message" do + expect(question.unanswered_error_message(log)).to eq( + I18n.t("validations.not_answered", question: question.error_display_label.downcase), + ) + end + end + + context "when no log is given" do + it "returns the default unanswered message" do + expect(question.unanswered_error_message).to eq( + I18n.t("validations.not_answered", question: question.error_display_label.downcase), + ) + end + 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 index 256531a65..64e82b744 100644 --- a/spec/models/form/lettings/questions/address_search_spec.rb +++ b/spec/models/form/lettings/questions/address_search_spec.rb @@ -98,4 +98,34 @@ RSpec.describe Form::Lettings::Questions::AddressSearch, type: :model do end end end + + describe "#unanswered_error_message" do + context "when the log is supported housing" do + let(:log) { build(:lettings_log, needstype: 2) } + + it "returns the confidential-scheme guidance message" do + expect(question.unanswered_error_message(log)).to eq( + "You must enter address. If your letting is in a confidential scheme, please check the scheme you chose in the ‘Set up this lettings log’ section. If a scheme needs updating to mark it as confidential, a CORE coordinator in your organisation can do this.", + ) + end + end + + context "when the log is general needs" do + let(:log) { build(:lettings_log, needstype: 1) } + + it "returns the default unanswered message" do + expect(question.unanswered_error_message(log)).to eq( + I18n.t("validations.not_answered", question: question.error_display_label.downcase), + ) + end + end + + context "when no log is given" do + it "returns the default unanswered message" do + expect(question.unanswered_error_message).to eq( + I18n.t("validations.not_answered", question: question.error_display_label.downcase), + ) + end + end + end end diff --git a/spec/models/lettings_log_derived_fields_spec.rb b/spec/models/lettings_log_derived_fields_spec.rb index 95a25fe08..50d0d86e5 100644 --- a/spec/models/lettings_log_derived_fields_spec.rb +++ b/spec/models/lettings_log_derived_fields_spec.rb @@ -1585,6 +1585,113 @@ RSpec.describe LettingsLog, type: :model do end end + describe "address field behaviour for confidential schemes", metadata: { year: 26 } do + let(:startdate) { collection_start_date_for_year(2026) } + + let(:uprn) { "123456789" } + let(:uprn_known) { 1 } + let(:uprn_confirmed) { 1 } + let(:address_line1) { "1 Test Street" } + let(:address_line2) { "Testville" } + let(:town_or_city) { "Testford" } + let(:county) { "Testshire" } + let(:postcode_full) { "SW1 1AA" } + let(:la) { "E07000105" } + let(:uprn_selection) { uprn } + let(:postcode_known) { 1 } + let(:manual_address_entry_selected) { true } + + around do |example| + Timecop.freeze(collection_start_date_for_year(2026)) do + Singleton.__init__(FormHandler) + example.run + end + end + + before do + log.needstype = 2 + log.assign_attributes(uprn:, uprn_known:, uprn_confirmed:, address_line1:, address_line2:, town_or_city:, county:, postcode_full:, la:, uprn_selection:, postcode_known:, manual_address_entry_selected:) + end + + context "when the scheme has confidential information" do + let(:confidential_scheme) { create(:scheme, sensitive: 1) } + let(:location) { create(:location, scheme: confidential_scheme) } + + before do + log.scheme = confidential_scheme + log.location = location + end + + it "does not ask the address or UPRN question" do + expect(log.is_address_asked?).to be false + end + + it "resets all the address and UPRN fields to nil" do + expect { log.set_derived_fields! } + .to change { log.read_attribute(:uprn) }.from(uprn).to(nil) + .and change { log.read_attribute(:uprn_known) }.from(uprn_known).to(nil) + .and change { log.read_attribute(:uprn_confirmed) }.from(uprn_confirmed).to(nil) + .and change { log.read_attribute(:address_line1) }.from(address_line1).to(nil) + .and change { log.read_attribute(:address_line2) }.from(address_line2).to(nil) + .and change { log.read_attribute(:town_or_city) }.from(town_or_city).to(nil) + .and change { log.read_attribute(:county) }.from(county).to(nil) + .and change { log.read_attribute(:postcode_full) }.from(postcode_full).to(nil) + .and change { log.read_attribute(:uprn_selection) }.from(uprn_selection).to(nil) + .and change { log.read_attribute(:postcode_known) }.from(postcode_known).to(nil) + .and change { log.read_attribute(:manual_address_entry_selected) }.from(manual_address_entry_selected).to(nil) + end + + context "when the log is a new-build first let" do + before do + log.rsnvac = 15 + end + + it "still does not ask the address or UPRN question (confidential overrides new-build)" do + expect(log.is_address_asked?).to be false + end + + it "resets all the address and UPRN fields to nil" do + expect { log.set_derived_fields! } + .to change { log.read_attribute(:uprn) }.from(uprn).to(nil) + .and change { log.read_attribute(:uprn_known) }.from(uprn_known).to(nil) + .and change { log.read_attribute(:uprn_confirmed) }.from(uprn_confirmed).to(nil) + .and change { log.read_attribute(:address_line1) }.from(address_line1).to(nil) + .and change { log.read_attribute(:address_line2) }.from(address_line2).to(nil) + .and change { log.read_attribute(:town_or_city) }.from(town_or_city).to(nil) + .and change { log.read_attribute(:county) }.from(county).to(nil) + .and change { log.read_attribute(:postcode_full) }.from(postcode_full).to(nil) + .and change { log.read_attribute(:town_or_city) }.from(town_or_city).to(nil) + .and change { log.read_attribute(:county) }.from(county).to(nil) + .and change { log.read_attribute(:postcode_full) }.from(postcode_full).to(nil) + .and change { log.read_attribute(:uprn_selection) }.from(uprn_selection).to(nil) + .and change { log.read_attribute(:postcode_known) }.from(postcode_known).to(nil) + .and change { log.read_attribute(:manual_address_entry_selected) }.from(manual_address_entry_selected).to(nil) + end + end + end + + context "when the scheme does not have confidential information" do + let(:non_confidential_scheme) { create(:scheme, sensitive: 0) } + let(:location) { create(:location, scheme: non_confidential_scheme) } + + before do + log.scheme = non_confidential_scheme + log.location = location + end + + it "ask the address or UPRN question" do + expect(log.is_address_asked?).to be true + end + + it "routes a new-build property down the manual address entry route" do + log.assign_attributes(manual_address_entry_selected: false, rsnvac: 15, uprn: nil) + + expect { log.set_derived_fields! } + .to change(log, :manual_address_entry_selected).from(false).to(true) + end + end + end + describe "#infer_at_most_one_relationship!" do context "when 2025", metadata: { year: 25 } do before do diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index a76d902a4..779b7be4a 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -2221,5 +2221,24 @@ RSpec.describe LettingsLog do end end end + + describe "#scheme_has_confidential_information?" do + let(:log) { build(:lettings_log) } + + it "returns false when there is no scheme" do + log.scheme = nil + expect(log.scheme_has_confidential_information?).to be false + end + + it "returns false when the scheme is not confidential" do + log.scheme = build(:scheme, sensitive: 0) + expect(log.scheme_has_confidential_information?).to be false + end + + it "returns true when the scheme is confidential" do + log.scheme = build(:scheme, sensitive: 1) + expect(log.scheme_has_confidential_information?).to be true + end + end end # rubocop:enable RSpec/MessageChain diff --git a/spec/requests/form/address_search_guidance_spec.rb b/spec/requests/form/address_search_guidance_spec.rb new file mode 100644 index 000000000..c68bfd8af --- /dev/null +++ b/spec/requests/form/address_search_guidance_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" + +RSpec.describe "Address search bottom guidance", type: :request do + let(:user) { create(:user) } + + before { sign_in user } + + context "with a 2026 lettings log at the address search question" do + let(:lettings_log) { create(:lettings_log, :completed, assigned_to: user, manual_address_entry_selected: false) } + + it "shows the confidential supported lettings guidance drop-down" do + get "/lettings-logs/#{lettings_log.id}/address-search" + + expect(response).to have_http_status(:ok) + expect(response.body).to include("What should I do for confidential supported lettings?") + expect(response.body).to include("Full address or UPRN is not required for confidential supported lettings.") + end + end + + context "with a 2026 sales log at the address search question" do + let(:sales_log) { create(:sales_log, :completed, assigned_to: user, manual_address_entry_selected: false) } + + it "does not show the lettings confidential supported lettings guidance" do + get "/sales-logs/#{sales_log.id}/address-search" + + expect(response.body).not_to include("What should I do for confidential supported lettings?") + end + end +end diff --git a/spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb index 665e3d683..adcc262fb 100644 --- a/spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb @@ -11,11 +11,14 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do 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) } + # Pinned non-confidential so the address/UPRN validations run as these tests expect; + # the confidential-scheme behaviour is covered by its own describe below (sensitive: 1). + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org, sensitive: 0) } let(:postcode_first_part) { "AA1".freeze } let(:postcode_second_part) { "1AA".freeze } let(:postcode) { "#{postcode_first_part} #{postcode_second_part}" } let(:location) { create(:location, :with_old_visible_id, scheme:, postcode:) } + let(:confidential_suffix) { I18n.t("validations.lettings.2026.bulk_upload.address.confidential_scheme_suffix") } let(:setup_section_params) do { @@ -1676,10 +1679,10 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do it "adds errors to missing key address fields" do parser.valid? - expect(parser.errors[:field_19]).to eql([I18n.t("validations.lettings.2026.bulk_upload.not_answered", question: "address line 1.")]) - expect(parser.errors[:field_21]).to eql([I18n.t("validations.lettings.2026.bulk_upload.not_answered", question: "town or city.")]) - expect(parser.errors[:field_23]).to eql([I18n.t("validations.lettings.2026.bulk_upload.not_answered", question: "part 1 of postcode.")]) - expect(parser.errors[:field_24]).to eql([I18n.t("validations.lettings.2026.bulk_upload.not_answered", question: "part 2 of postcode.")]) + expect(parser.errors[:field_19]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.not_answered', question: 'address line 1.')} #{confidential_suffix}"]) + expect(parser.errors[:field_21]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.not_answered', question: 'town or city.')} #{confidential_suffix}"]) + expect(parser.errors[:field_23]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.not_answered', question: 'part 1 of postcode.')} #{confidential_suffix}"]) + expect(parser.errors[:field_24]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.not_answered', question: 'part 2 of postcode.')} #{confidential_suffix}"]) end end @@ -1707,11 +1710,20 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do it "adds appropriate errors to UPRN and key address fields" do parser.valid? - expect(parser.errors[:field_18]).to eql([I18n.t("validations.lettings.2026.bulk_upload.address.not_answered")]) - expect(parser.errors[:field_19]).to eql([I18n.t("validations.lettings.2026.bulk_upload.address.not_answered")]) - expect(parser.errors[:field_21]).to eql([I18n.t("validations.lettings.2026.bulk_upload.address.not_answered")]) - expect(parser.errors[:field_23]).to eql([I18n.t("validations.lettings.2026.bulk_upload.address.not_answered")]) - expect(parser.errors[:field_24]).to eql([I18n.t("validations.lettings.2026.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_18]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.address.not_answered')} #{confidential_suffix}"]) + expect(parser.errors[:field_19]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.address.not_answered')} #{confidential_suffix}"]) + expect(parser.errors[:field_21]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.address.not_answered')} #{confidential_suffix}"]) + expect(parser.errors[:field_23]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.address.not_answered')} #{confidential_suffix}"]) + expect(parser.errors[:field_24]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.address.not_answered')} #{confidential_suffix}"]) + end + + it "appends the confidential scheme guidance to each address error" do + parser.valid? + %i[field_18 field_19 field_21 field_23 field_24].each do |field| + expect(parser.errors[field].first).to end_with( + "If your letting is in a confidential scheme, check the scheme you chose. A coordinator can edit a scheme to confidential on the ‘Schemes' page.", + ) + end end end @@ -1720,8 +1732,8 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do 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.2026.bulk_upload.address.not_answered")]) - expect(parser.errors[:field_19]).to eql([I18n.t("validations.lettings.2026.bulk_upload.address.not_answered")]) + expect(parser.errors[:field_18]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.address.not_answered')} #{confidential_suffix}"]) + expect(parser.errors[:field_19]).to eql(["#{I18n.t('validations.lettings.2026.bulk_upload.address.not_answered')} #{confidential_suffix}"]) expect(parser.errors[:field_21]).to be_empty expect(parser.errors[:field_23]).to be_empty expect(parser.errors[:field_24]).to be_empty @@ -1864,6 +1876,70 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do end end + describe "UPRN and address fields for a supported housing log in a confidential scheme" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org, sensitive: 1) } + let(:base_attributes) do + setup_section_params.merge({ + field_4: 2, + field_5: "S#{scheme.id}", + field_6: location.old_visible_id, + }) + end + + context "when no UPRN or address fields are provided" do + let(:attributes) { base_attributes.merge({ field_18: nil, field_19: nil, field_21: nil, field_23: nil, field_24: nil }) } + + it "does not require the address or UPRN (no not answered errors)" do + parser.valid? + %i[field_18 field_19 field_21 field_23 field_24].each do |field| + expect(parser.errors[field]).to be_empty + end + end + + it "does not import any address or UPRN fields" do + log = parser.log + log.valid? + expect(log.read_attribute(:uprn)).to be_nil + expect(log.read_attribute(:address_line1)).to be_nil + expect(log.read_attribute(:town_or_city)).to be_nil + expect(log.read_attribute(:postcode_full)).to be_nil + end + + it "derives the local authority from the scheme's location" do + log = parser.log + log.valid? + expect(log.la).to eq(location.location_code) + end + end + + context "when address and UPRN fields are provided in the template" do + let(:attributes) do + base_attributes.merge({ + field_18: "123456789012", + field_19: "1 Test Street", + field_21: "Testville", + field_23: postcode_first_part, + field_24: postcode_second_part, + field_25: "E09000008", + }) + end + + it "ignores them and does not import the address or UPRN fields" do + log = parser.log + log.valid? + expect(log.read_attribute(:uprn)).to be_nil + expect(log.read_attribute(:address_line1)).to be_nil + expect(log.read_attribute(:postcode_full)).to be_nil + end + + it "still derives the local authority from the scheme's location, ignoring the template LA" do + log = parser.log + log.valid? + expect(log.la).to eq(location.location_code) + 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" }) }