Browse Source

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
pull/3325/merge
Oscar Richardson 3 days ago committed by GitHub
parent
commit
1520767dd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      app/controllers/form_controller.rb
  2. 7
      app/models/derived_variables/lettings_log_variables.rb
  3. 6
      app/models/form/lettings/questions/address_line1.rb
  4. 6
      app/models/form/lettings/questions/address_search.rb
  5. 2
      app/models/form/lettings/questions/declaration.rb
  6. 2
      app/models/form/lettings/questions/uprn.rb
  7. 2
      app/models/form/lettings/questions/uprn_known.rb
  8. 2
      app/models/form/question.rb
  9. 2
      app/models/form/sales/questions/privacy_notice.rb
  10. 2
      app/models/form/sales/questions/uprn.rb
  11. 2
      app/models/form/sales/questions/uprn_known.rb
  12. 8
      app/models/lettings_log.rb
  13. 4
      app/models/scheme.rb
  14. 66
      app/services/bulk_upload/lettings/year2026/row_parser.rb
  15. 6
      app/views/form/guidance/_address_search.html.erb
  16. 4
      config/locales/forms/2026/lettings/guidance.en.yml
  17. 1
      config/locales/validations/lettings/2026/bulk_upload.en.yml
  18. 4
      config/locales/validations/lettings/property_information.en.yml
  19. 67
      spec/models/form/lettings/pages/property_local_authority_spec.rb
  20. 30
      spec/models/form/lettings/questions/address_line1_spec.rb
  21. 30
      spec/models/form/lettings/questions/address_search_spec.rb
  22. 107
      spec/models/lettings_log_derived_fields_spec.rb
  23. 19
      spec/models/lettings_log_spec.rb
  24. 29
      spec/requests/form/address_search_guidance_spec.rb
  25. 100
      spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb

2
app/controllers/form_controller.rb

@ -35,7 +35,7 @@ class FormController < ApplicationController
else else
@log.valid? if mandatory_questions_with_no_response.any? @log.valid? if mandatory_questions_with_no_response.any?
mandatory_questions_with_no_response.map do |question| 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 end
error_attributes = @log.errors.map(&:attribute) error_attributes = @log.errors.map(&:attribute)
Rails.logger.info "User triggered validation(s) on: #{error_attributes.join(', ')}" Rails.logger.info "User triggered validation(s) on: #{error_attributes.join(', ')}"

7
app/models/derived_variables/lettings_log_variables.rb

@ -181,6 +181,13 @@ module DerivedVariables::LettingsLogVariables
self.la = nil self.la = nil
end 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? clear_gender_description_unless_gender_not_same_as_sex! if form.start_year_2026_or_later?
set_checkbox_values! set_checkbox_values!

6
app/models/form/lettings/questions/address_line1.rb

@ -18,5 +18,11 @@ class Form::Lettings::Questions::AddressLine1 < ::Form::Question
].select(&:present?).join("\n") ].select(&:present?).join("\n")
end 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 QUESTION_NUMBER_FROM_YEAR = { 2023 => 12, 2024 => 13, 2025 => 17, 2026 => 17 }.freeze
end end

6
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"] } || {} answer_options(log, user).transform_values { |value| value["value"] } || {}
end 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 QUESTION_NUMBER_FROM_YEAR = { 2024 => 12, 2025 => 16, 2026 => 16 }.freeze
end end

2
app/models/form/lettings/questions/declaration.rb

@ -18,7 +18,7 @@ class Form::Lettings::Questions::Declaration < ::Form::Question
{ "declaration" => { "value" => declaration_text } }.freeze { "declaration" => { "value" => declaration_text } }.freeze
end end
def unanswered_error_message def unanswered_error_message(_log = nil)
if form.start_year_2024_or_later? if form.start_year_2024_or_later?
I18n.t("validations.declaration.missing.post_2024") I18n.t("validations.declaration.missing.post_2024")
else else

2
app/models/form/lettings/questions/uprn.rb

@ -14,7 +14,7 @@ class Form::Lettings::Questions::Uprn < ::Form::Question
] ]
end end
def unanswered_error_message def unanswered_error_message(_log = nil)
I18n.t("validations.lettings.property.uprn.invalid") I18n.t("validations.lettings.property.uprn.invalid")
end end

2
app/models/form/lettings/questions/uprn_known.rb

@ -26,7 +26,7 @@ class Form::Lettings::Questions::UprnKnown < ::Form::Question
"0" => { "value" => "No" }, "0" => { "value" => "No" },
}.freeze }.freeze
def unanswered_error_message def unanswered_error_message(_log = nil)
I18n.t("validations.property.uprn_known.invalid") I18n.t("validations.property.uprn_known.invalid")
end end

2
app/models/form/question.rb

@ -240,7 +240,7 @@ class Form::Question
format_ending(label) format_ending(label)
end end
def unanswered_error_message def unanswered_error_message(_log = nil)
question_text = error_display_label.presence || "this question." question_text = error_display_label.presence || "this question."
I18n.t("validations.not_answered", question: question_text.downcase) I18n.t("validations.not_answered", question: question_text.downcase)
end end

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

@ -19,7 +19,7 @@ class Form::Sales::Questions::PrivacyNotice < ::Form::Question
{ "privacynotice" => { "value" => declaration_text } }.freeze { "privacynotice" => { "value" => declaration_text } }.freeze
end end
def unanswered_error_message def unanswered_error_message(_log = nil)
buyer_or_buyers = @joint_purchase ? "buyers" : "buyer" buyer_or_buyers = @joint_purchase ? "buyers" : "buyer"
if form.start_year_2024_or_later? if form.start_year_2024_or_later?
I18n.t("validations.privacynotice.missing.post_2024", buyer_or_buyers:) I18n.t("validations.privacynotice.missing.post_2024", buyer_or_buyers:)

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

@ -14,7 +14,7 @@ class Form::Sales::Questions::Uprn < ::Form::Question
] ]
end end
def unanswered_error_message def unanswered_error_message(_log = nil)
I18n.t("validations.sales.property_information.uprn.invalid") I18n.t("validations.sales.property_information.uprn.invalid")
end end

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

@ -26,7 +26,7 @@ class Form::Sales::Questions::UprnKnown < ::Form::Question
"0" => { "value" => "No" }, "0" => { "value" => "No" },
}.freeze }.freeze
def unanswered_error_message def unanswered_error_message(_log = nil)
I18n.t("validations.sales.property_information.uprn_known.invalid") I18n.t("validations.sales.property_information.uprn_known.invalid")
end end

8
app/models/lettings_log.rb

@ -822,7 +822,13 @@ class LettingsLog < Log
end end
def is_address_asked? 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 end
def referral_is_from_local_authority_housing_register? def referral_is_from_local_authority_housing_register?

4
app/models/scheme.rb

@ -367,6 +367,10 @@ class Scheme < ApplicationRecord
status_at(6.months.from_now) == :deactivating_soon status_at(6.months.from_now) == :deactivating_soon
end end
def has_confidential_information?
sensitive == "Yes"
end
def discard! def discard!
update!(discarded_at: Time.zone.now) update!(discarded_at: Time.zone.now)
locations.each(&:discard!) locations.each(&:discard!)

66
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_assigned_to_when_support, on: :after_log
validate :validate_all_charges_given, 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_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log, unless: :scheme_has_confidential_information?
validate :validate_address_fields, on: :after_log validate :validate_address_fields, on: :after_log, unless: :scheme_has_confidential_information?
validate :validate_nationality, on: :after_log validate :validate_nationality, on: :after_log
validate :validate_reasonpref_reason_values, 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 def validate_uprn_exists_if_any_key_address_fields_are_blank
if field_18.blank? && !key_address_fields_provided? if field_18.blank? && !key_address_fields_provided?
%i[field_19 field_21 field_23 field_24].each do |field| %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 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
end end
@ -701,23 +701,27 @@ private
def validate_address_fields def validate_address_fields
if field_18.blank? || log.errors.attribute_names.include?(:uprn) if field_18.blank? || log.errors.attribute_names.include?(:uprn)
if field_19.blank? && errors[:field_19].blank? 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 end
if field_21.blank? && errors[:field_21].blank? 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 end
if field_23.blank? && errors[:field_23].blank? 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 end
if field_24.blank? && errors[:field_24].blank? 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 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 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 = log.form.questions.filter { |q| q.type == "interruption_screen" && q.page.routed_to?(log, nil) }.compact
routed_to_soft_validation_questions.each do |question| 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["first_time_property_let_as_social_housing"] = first_time_property_let_as_social_housing
attributes["uprn_known"] = field_18.present? ? 1 : 0 unless scheme_has_confidential_information?
attributes["uprn_confirmed"] = 1 if field_18.present? attributes["uprn_known"] = field_18.present? ? 1 : 0
attributes["skip_update_uprn_confirmed"] = true attributes["uprn_confirmed"] = 1 if field_18.present?
attributes["uprn"] = field_18 attributes["skip_update_uprn_confirmed"] = true
attributes["address_line1"] = field_19 attributes["uprn"] = field_18
attributes["address_line1_as_entered"] = field_19 attributes["address_line1"] = field_19
attributes["address_line2"] = field_20 attributes["address_line1_as_entered"] = field_19
attributes["address_line2_as_entered"] = field_20 attributes["address_line2"] = field_20
attributes["town_or_city"] = field_21 attributes["address_line2_as_entered"] = field_20
attributes["town_or_city_as_entered"] = field_21 attributes["town_or_city"] = field_21
attributes["county"] = field_22 attributes["town_or_city_as_entered"] = field_21
attributes["county_as_entered"] = field_22 attributes["county"] = field_22
attributes["postcode_full"] = postcode_full attributes["county_as_entered"] = field_22
attributes["postcode_full_as_entered"] = postcode_full attributes["postcode_full"] = postcode_full
attributes["postcode_known"] = postcode_known attributes["postcode_full_as_entered"] = postcode_full
attributes["la"] = field_25 attributes["postcode_known"] = postcode_known
attributes["la_as_entered"] = field_25 attributes["la"] = field_25
attributes["address_line1_input"] = address_line1_input attributes["la_as_entered"] = field_25
attributes["postcode_full_input"] = postcode_full attributes["address_line1_input"] = address_line1_input
attributes["select_best_address_match"] = true if field_18.blank? 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_same_as_sex1"] = field_43
attributes["gender_description1"] = field_44 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) @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 end
def scheme_has_confidential_information?
!!scheme&.has_confidential_information?
end
def location def location
return if scheme.nil? return if scheme.nil?

6
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 %> <%= I18n.t("forms.#{@log.form.start_date.year}.#{@log.form.type}.guidance.address_uprn.content").html_safe %>
<% end %> <% 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 %>
<div class="govuk-button-group"> <div class="govuk-button-group">
<%= govuk_link_to "Enter the address manually instead", address_manual_input_path(@log.log_type, @log.id), class: "govuk-button govuk-button--secondary" %> <%= govuk_link_to "Enter the address manually instead", address_manual_input_path(@log.log_type, @log.id), class: "govuk-button govuk-button--secondary" %>
</div> </div>

4
config/locales/forms/2026/lettings/guidance.en.yml

@ -73,6 +73,10 @@ en:
content: "<p>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.</p> content: "<p>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.</p>
<p>The UPRN may not be the same as the property reference assigned by your organisation.</p>" <p>The UPRN may not be the same as the property reference assigned by your organisation.</p>"
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: needs_type:
title: "What does each need type mean?" title: "What does each need type mean?"
content: "General needs housing includes both self-contained and shared housing without support or specific adaptations.<br><br>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." content: "General needs housing includes both self-contained and shared housing without support or specific adaptations.<br><br>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."

1
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." 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." 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." 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: nationality:
invalid: "Select a valid nationality." invalid: "Select a valid nationality."
charges: charges:

4
config/locales/validations/lettings/property_information.en.yml

@ -2,6 +2,10 @@ en:
validations: validations:
lettings: lettings:
property: 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: postcode_full:
invalid: "Enter a postcode in the correct format, for example AA1 1AA." 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." not_in_england: "It looks like you have an entered a postcode outside of England. Only create logs for lettings in England."

67
spec/models/form/lettings/pages/property_local_authority_spec.rb

@ -33,28 +33,65 @@ RSpec.describe Form::Lettings::Pages::PropertyLocalAuthority, type: :model do
end end
context "when routing to the page" do context "when routing to the page" do
let(:log) { build(:lettings_log) }
before do before do
allow(form).to receive(:start_year_2025_or_later?).and_return(true) allow(form).to receive(:start_year_2025_or_later?).and_return(true)
end end
it "is routed to when la is not inferred and it is general needs log" do context "when the log is general needs" do
log.needstype = 1 let(:log) { build(:lettings_log, needstype: 1) }
log.is_la_inferred = false
expect(page).to be_routed_to(log, nil) it "is not routed to when `is_la_inferred` is nil" do
end 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 it "is not routed to when LA is inferred" do
log.needstype = 1 log.is_la_inferred = true
log.is_la_inferred = true expect(page).not_to be_routed_to(log, nil)
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 end
it "is not routed to when it's a supported housing log" do context "when the log is supported housing" do
log.needstype = 2 let(:log) { build(:lettings_log, needstype: 2) }
log.is_la_inferred = false
expect(page).not_to be_routed_to(log, nil) 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 end
end end

30
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 it "has the correct check_answers_card_number" do
expect(question.check_answers_card_number).to be_nil expect(question.check_answers_card_number).to be_nil
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 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 end

30
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 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 end

107
spec/models/lettings_log_derived_fields_spec.rb

@ -1585,6 +1585,113 @@ RSpec.describe LettingsLog, type: :model do
end end
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 describe "#infer_at_most_one_relationship!" do
context "when 2025", metadata: { year: 25 } do context "when 2025", metadata: { year: 25 } do
before do before do

19
spec/models/lettings_log_spec.rb

@ -2221,5 +2221,24 @@ RSpec.describe LettingsLog do
end end
end 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 end
# rubocop:enable RSpec/MessageChain # rubocop:enable RSpec/MessageChain

29
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

100
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(:owning_org) { create(:organisation, :with_old_visible_id) }
let(:managing_org) { create(:organisation, :with_old_visible_id, rent_periods: [4, 1]) } 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_first_part) { "AA1".freeze }
let(:postcode_second_part) { "1AA".freeze } let(:postcode_second_part) { "1AA".freeze }
let(:postcode) { "#{postcode_first_part} #{postcode_second_part}" } let(:postcode) { "#{postcode_first_part} #{postcode_second_part}" }
let(:location) { create(:location, :with_old_visible_id, scheme:, postcode:) } 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 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 it "adds errors to missing key address fields" do
parser.valid? 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_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.")]) 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.")]) 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.")]) 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
end end
@ -1707,11 +1710,20 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
it "adds appropriate errors to UPRN and key address fields" do it "adds appropriate errors to UPRN and key address fields" do
parser.valid? parser.valid?
expect(parser.errors[:field_18]).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")]) 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")]) 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")]) 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")]) 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
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 it "adds errors to UPRN and the missing key address field" do
parser.valid? parser.valid?
expect(parser.errors[:field_18]).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")]) 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_21]).to be_empty
expect(parser.errors[:field_23]).to be_empty expect(parser.errors[:field_23]).to be_empty
expect(parser.errors[:field_24]).to be_empty expect(parser.errors[:field_24]).to be_empty
@ -1864,6 +1876,70 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
end end
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 describe "#field_17" do # unitletas
context "when no longer a valid option from previous year" do context "when no longer a valid option from previous year" do
let(:attributes) { setup_section_params.merge({ field_17: "4" }) } let(:attributes) { setup_section_params.merge({ field_17: "4" }) }

Loading…
Cancel
Save