Browse Source

CLDC-4461: Hide confidential addresses - fix confidential to non confidential edge case (#3373)

* Fix bug where the UPRN/address questions were not shown after switching from a confidential to a non-confidential scheme

* Improve tests

* Hide address data in CSV download for logs in confidential schemes

* Action PR comments

* Action second round of PR comments
main v0.6.15
Oscar Richardson 2 days ago committed by GitHub
parent
commit
669f8338c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      app/models/derived_variables/lettings_log_variables.rb
  2. 38
      app/services/csv/lettings_log_csv_service.rb
  3. 73
      spec/models/form/lettings/pages/address_fallback_spec.rb
  4. 73
      spec/models/form/lettings/pages/address_search_spec.rb
  5. 42
      spec/models/lettings_log_derived_fields_spec.rb
  6. 13
      spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb
  7. 76
      spec/services/csv/lettings_log_csv_service_spec.rb

1
app/models/derived_variables/lettings_log_variables.rb

@ -185,7 +185,6 @@ module DerivedVariables::LettingsLogVariables
reset_address_fields! reset_address_fields!
self.uprn_selection = nil self.uprn_selection = nil
self.postcode_known = nil self.postcode_known = nil
self.manual_address_entry_selected = nil
end 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?

38
app/services/csv/lettings_log_csv_service.rb

@ -280,6 +280,32 @@ module Csv
SCHEME_AND_LOCATION_ATTRIBUTES = %w[scheme_code scheme_service_name scheme_confidential SCHTYPE scheme_registered_under_care_act scheme_owning_organisation_name scheme_primary_client_group scheme_has_other_client_group scheme_secondary_client_group scheme_support_type scheme_intended_stay scheme_created_at location_code location_postcode location_name location_units location_type_of_unit location_mobility_type location_local_authority location_startdate].freeze SCHEME_AND_LOCATION_ATTRIBUTES = %w[scheme_code scheme_service_name scheme_confidential SCHTYPE scheme_registered_under_care_act scheme_owning_organisation_name scheme_primary_client_group scheme_has_other_client_group scheme_secondary_client_group scheme_support_type scheme_intended_stay scheme_created_at location_code location_postcode location_name location_units location_type_of_unit location_mobility_type location_local_authority location_startdate].freeze
# TODO: (CLDC-4462): delete once address data for logs in confidential schemes is wiped.
# Interim measure: logs in a confidential scheme that were created before the
# confidential address feature may still hold property address/UPRN data that is no
# longer collected. Blank those columns in the download until the data is wiped (the
# local authority, derived from the scheme location, is intentionally retained).
ADDRESS_FIELDS_HIDDEN_FOR_CONFIDENTIAL_SCHEME = %w[
uprn
uprn_known
uprn_confirmed
uprn_selection
address_line1
address_line2
town_or_city
county
postcode_full
postcode_known
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
def lettings_log_attributes def lettings_log_attributes
ordered_questions = FormHandler.instance.ordered_questions_for_year(@year, "lettings") ordered_questions = FormHandler.instance.ordered_questions_for_year(@year, "lettings")
soft_validations_attributes = soft_validations_attributes(ordered_questions) soft_validations_attributes = soft_validations_attributes(ordered_questions)
@ -344,6 +370,8 @@ module Csv
def value(attribute, log) def value(attribute, log)
attribute = "rent_type" if attribute == "rent_type_detail" # rent_type_detail is the requested column header for rent_type, so as not to confuse with renttype. It can be exported as label or code. attribute = "rent_type" if attribute == "rent_type_detail" # rent_type_detail is the requested column header for rent_type, so as not to confuse with renttype. It can be exported as label or code.
return nil if hide_confidential_scheme_address?(attribute, log) # TODO: (CLDC-4462): delete once address data for logs in confidential schemes is wiped.
if CUSTOM_CALL_CHAINS.key? attribute.to_sym if CUSTOM_CALL_CHAINS.key? attribute.to_sym
call_chain = CUSTOM_CALL_CHAINS[attribute.to_sym][@export_type.to_sym] call_chain = CUSTOM_CALL_CHAINS[attribute.to_sym][@export_type.to_sym]
call_chain.reduce(log) { |object, next_call| object&.public_send(next_call) } call_chain.reduce(log) { |object, next_call| object&.public_send(next_call) }
@ -376,6 +404,16 @@ module Csv
end end
end end
# TODO: (CLDC-4462): delete once address data for logs in confidential schemes is wiped.
def hide_confidential_scheme_address?(attribute, log)
ADDRESS_FIELDS_HIDDEN_FOR_CONFIDENTIAL_SCHEME.include?(attribute) && confidential_scheme_ids.include?(log.scheme_id)
end
# TODO: (CLDC-4462): delete once address data for logs in confidential schemes is wiped.
def confidential_scheme_ids
@confidential_scheme_ids ||= Scheme.where(sensitive: "Yes").pluck(:id).to_set
end
def person_details_not_known?(log, attribute) def person_details_not_known?(log, attribute)
details_known_field = PERSON_DETAILS.find { |key, _value| key == attribute }[1]["details_known_field"] details_known_field = PERSON_DETAILS.find { |key, _value| key == attribute }[1]["details_known_field"]
log[details_known_field] == 1 # 1 for lettings logs, 2 for sales logs log[details_known_field] == 1 # 1 for lettings logs, 2 for sales logs

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

@ -26,4 +26,77 @@ RSpec.describe Form::Lettings::Pages::AddressFallback, type: :model do
it "has correct depends_on" do it "has correct depends_on" do
expect(page.depends_on).to eq([{ "manual_address_entry_selected" => true, "is_address_asked?" => true }]) expect(page.depends_on).to eq([{ "manual_address_entry_selected" => true, "is_address_asked?" => true }])
end end
context "when routing to the page" do
let(:form) { FormHandler.instance.forms["current_lettings"] }
let(:subsection) { instance_double(Form::Subsection, form:, enabled?: true) }
context "when the log is general needs" do
let(:log) { build(:lettings_log, needstype: 1) }
it "is routed to when the address is being entered manually" do
log.manual_address_entry_selected = true
expect(page).to be_routed_to(log, nil)
end
it "is not routed to when the address is not being entered manually" do
log.manual_address_entry_selected = false
expect(page).not_to be_routed_to(log, nil)
end
it "is not routed to when `manual_address_entry_selected` is nil" do
log.manual_address_entry_selected = nil
expect(page).not_to be_routed_to(log, nil)
end
end
context "when the log is supported housing" do
let(:log) { build(:lettings_log, needstype: 2) }
context "and the collection year is 2026 or later" do
before do
allow(form).to receive(:start_year_2026_or_later?).and_return(true)
end
it "is routed to when the address is being entered manually" do
log.manual_address_entry_selected = true
expect(page).to be_routed_to(log, nil)
end
it "is not routed to when the address is not being entered manually" do
log.manual_address_entry_selected = false
expect(page).not_to be_routed_to(log, nil)
end
it "is not routed to when `manual_address_entry_selected` is nil" do
log.manual_address_entry_selected = nil
expect(page).not_to be_routed_to(log, nil)
end
end
context "and the collection year is before 2026" do
before do
allow(form).to receive(:start_year_2026_or_later?).and_return(false)
end
it "is not routed to, even when the address is being entered manually" do
log.manual_address_entry_selected = true
expect(page).not_to be_routed_to(log, nil)
end
end
context "when the scheme has confidential information" do
let(:log) { build(:lettings_log, needstype: 2, scheme: build(:scheme, sensitive: 1)) }
before do
allow(form).to receive(:start_year_2026_or_later?).and_return(true)
end
it "is not routed to, even when the address is being entered manually" do
log.manual_address_entry_selected = true
expect(page).not_to be_routed_to(log, nil)
end
end
end
end
end end

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

@ -39,4 +39,77 @@ RSpec.describe Form::Lettings::Pages::AddressSearch, type: :model do
expect(page.question_number).to eq(16) expect(page.question_number).to eq(16)
end end
end end
context "when routing to the page" do
let(:form) { FormHandler.instance.forms["current_lettings"] }
let(:subsection) { instance_double(Form::Subsection, form:, enabled?: true) }
context "when the log is general needs" do
let(:log) { build(:lettings_log, needstype: 1) }
it "is routed to when the address is not being entered manually" do
log.manual_address_entry_selected = false
expect(page).to be_routed_to(log, nil)
end
it "is not routed to when the address is being entered manually" do
log.manual_address_entry_selected = true
expect(page).not_to be_routed_to(log, nil)
end
it "is not routed to when `manual_address_entry_selected` is nil" do
log.manual_address_entry_selected = nil
expect(page).not_to be_routed_to(log, nil)
end
end
context "when the log is supported housing" do
let(:log) { build(:lettings_log, needstype: 2) }
context "and the collection year is 2026 or later" do
before do
allow(form).to receive(:start_year_2026_or_later?).and_return(true)
end
it "is routed to when the address is not being entered manually" do
log.manual_address_entry_selected = false
expect(page).to be_routed_to(log, nil)
end
it "is not routed to when the address is being entered manually" do
log.manual_address_entry_selected = true
expect(page).not_to be_routed_to(log, nil)
end
it "is not routed to when `manual_address_entry_selected` is nil" do
log.manual_address_entry_selected = nil
expect(page).not_to be_routed_to(log, nil)
end
end
context "and the collection year is before 2026" do
before do
allow(form).to receive(:start_year_2026_or_later?).and_return(false)
end
it "is not routed to, even when the address is not being entered manually" do
log.manual_address_entry_selected = false
expect(page).not_to be_routed_to(log, nil)
end
end
context "when the scheme has confidential information" do
let(:log) { build(:lettings_log, needstype: 2, scheme: build(:scheme, sensitive: 1)) }
before do
allow(form).to receive(:start_year_2026_or_later?).and_return(true)
end
it "is not routed to, even when the address is not being entered manually" do
log.manual_address_entry_selected = false
expect(page).not_to be_routed_to(log, nil)
end
end
end
end
end end

42
spec/models/lettings_log_derived_fields_spec.rb

@ -1637,8 +1637,12 @@ RSpec.describe LettingsLog, type: :model do
.and change { log.read_attribute(:county) }.from(county).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(:postcode_full) }.from(postcode_full).to(nil)
.and change { log.read_attribute(:uprn_selection) }.from(uprn_selection).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(:postcode_known) }.from(postcode_known).to(nil))
.and change { log.read_attribute(:manual_address_entry_selected) }.from(manual_address_entry_selected).to(nil) end
it "does not reset `manual_address_entry_selected`" do
expect { log.set_derived_fields! }
.not_to(change { log.read_attribute(:manual_address_entry_selected) })
end end
context "when the log is a new-build first let" do context "when the log is a new-build first let" do
@ -1664,8 +1668,12 @@ RSpec.describe LettingsLog, type: :model do
.and change { log.read_attribute(:county) }.from(county).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(:postcode_full) }.from(postcode_full).to(nil)
.and change { log.read_attribute(:uprn_selection) }.from(uprn_selection).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(:postcode_known) }.from(postcode_known).to(nil))
.and change { log.read_attribute(:manual_address_entry_selected) }.from(manual_address_entry_selected).to(nil) end
it "does not reset `manual_address_entry_selected`" do
expect { log.set_derived_fields! }
.not_to(change { log.read_attribute(:manual_address_entry_selected) })
end end
end end
end end
@ -1690,6 +1698,32 @@ RSpec.describe LettingsLog, type: :model do
.to change(log, :manual_address_entry_selected).from(false).to(true) .to change(log, :manual_address_entry_selected).from(false).to(true)
end end
end end
context "when a log is changed from a confidential to a non-confidential scheme" do
# The confidential logic must leave manual_address_entry_selected in a
# routable state (either true or false), otherwise neither address page routes after the
# switch and the address question is never shown again.
let(:confidential_scheme) { create(:scheme, sensitive: 1) }
let(:non_confidential_scheme) { create(:scheme, sensitive: 0) }
let(:confidential_location) { create(:location, scheme: confidential_scheme) }
let(:non_confidential_location) { create(:location, scheme: non_confidential_scheme) }
before do
log.assign_attributes(manual_address_entry_selected: true)
log.scheme = confidential_scheme
log.location = confidential_location
log.set_derived_fields!
log.scheme = non_confidential_scheme
log.location = non_confidential_location
log.set_derived_fields!
end
it "asks the address question again with a routable value for `manual_address_entry_selected` (i.e., not nil)" do
expect(log.is_address_asked?).to be true
expect(log.manual_address_entry_selected).to be true
end
end
end end
describe "#infer_at_most_one_relationship!" do describe "#infer_at_most_one_relationship!" do

13
spec/services/bulk_upload/lettings/year2026/row_parser_spec.rb

@ -12,7 +12,7 @@ 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]) }
# Pinned non-confidential so the address/UPRN validations run as these tests expect; # 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). # 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(: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 }
@ -1887,11 +1887,11 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
end end
context "when no UPRN or address fields are provided" do 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 }) } let(:attributes) { base_attributes.merge({ field_18: nil, field_19: nil, field_20: nil, field_21: nil, field_22: nil, field_23: nil, field_24: nil }) }
it "does not require the address or UPRN (no not answered errors)" do it "does not require the address or UPRN (no not answered errors)" do
parser.valid? parser.valid?
%i[field_18 field_19 field_21 field_23 field_24].each do |field| %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 expect(parser.errors[field]).to be_empty
end end
end end
@ -1901,7 +1901,9 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
log.valid? log.valid?
expect(log.read_attribute(:uprn)).to be_nil expect(log.read_attribute(:uprn)).to be_nil
expect(log.read_attribute(:address_line1)).to be_nil expect(log.read_attribute(:address_line1)).to be_nil
expect(log.read_attribute(:address_line2)).to be_nil
expect(log.read_attribute(:town_or_city)).to be_nil expect(log.read_attribute(:town_or_city)).to be_nil
expect(log.read_attribute(:county)).to be_nil
expect(log.read_attribute(:postcode_full)).to be_nil expect(log.read_attribute(:postcode_full)).to be_nil
end end
@ -1917,7 +1919,9 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
base_attributes.merge({ base_attributes.merge({
field_18: "123456789012", field_18: "123456789012",
field_19: "1 Test Street", field_19: "1 Test Street",
field_20: "Test District",
field_21: "Testville", field_21: "Testville",
field_22: "Testshire",
field_23: postcode_first_part, field_23: postcode_first_part,
field_24: postcode_second_part, field_24: postcode_second_part,
field_25: "E09000008", field_25: "E09000008",
@ -1929,6 +1933,9 @@ RSpec.describe BulkUpload::Lettings::Year2026::RowParser do
log.valid? log.valid?
expect(log.read_attribute(:uprn)).to be_nil expect(log.read_attribute(:uprn)).to be_nil
expect(log.read_attribute(:address_line1)).to be_nil expect(log.read_attribute(:address_line1)).to be_nil
expect(log.read_attribute(:address_line2)).to be_nil
expect(log.read_attribute(:town_or_city)).to be_nil
expect(log.read_attribute(:county)).to be_nil
expect(log.read_attribute(:postcode_full)).to be_nil expect(log.read_attribute(:postcode_full)).to be_nil
end end

76
spec/services/csv/lettings_log_csv_service_spec.rb

@ -193,6 +193,82 @@ RSpec.describe Csv::LettingsLogCsvService do
end end
end end
# TODO: (CLDC-4462): delete once address data for logs in confidential schemes is wiped.
describe "confidential scheme behaviour" do
let(:year) { 2026 }
let(:owning_organisation) { create(:organisation) }
let(:scheme) { create(:scheme, sensitive: 1, owning_organisation:) }
let(:location) { create(:location, scheme:) }
# Every hidden field that actually appears as a column in this export.
let(:hidden_columns) { described_class::ADDRESS_FIELDS_HIDDEN_FOR_CONFIDENTIAL_SCHEME & attribute_line }
let(:log) do
create(
:lettings_log,
:ignore_validation_errors,
needstype: 2,
owning_organisation:,
managing_organisation: owning_organisation,
assigned_to: user,
scheme:,
location:,
startdate: Time.zone.local(2026, 5, 1),
).tap do |confidential_log|
# Simulate a log created before the confidential address feature that still holds
# property address data in the database. Populate every hidden address column so
# that the blanking is observable (a nil column would pass the assertion vacuously).
confidential_log.update_columns(
uprn: "123456789012",
uprn_known: 1,
uprn_confirmed: 1,
uprn_selection: "123456789012",
address_line1: "1 Secret Street",
address_line2: "Flat 2",
town_or_city: "Secretville",
county: "Secretshire",
postcode_full: "AB1 2CD",
postcode_known: 1,
address_line1_input: "1 Secret Street input",
postcode_full_input: "AB1 2CD",
address_line1_as_entered: "1 Secret Street as entered",
address_line2_as_entered: "Flat 2 as entered",
town_or_city_as_entered: "Secretville as entered",
county_as_entered: "Secretshire as entered",
postcode_full_as_entered: "AB1 2CD",
la_as_entered: "la as entered",
la: "E09000003",
)
end
end
def csv_value(attribute)
content_line[attribute_line.index(attribute)]
end
context "when a log's scheme is confidential" do
it "blanks every hidden address and UPRN column" do
expect(hidden_columns).not_to be_empty
hidden_columns.each do |attribute|
expect(csv_value(attribute)).to be_nil, "expected the #{attribute} column to be blank for a confidential-scheme log"
end
end
it "still exports the local authority" do
expect(csv_value("la")).to eq("E09000003")
end
end
context "when the scheme is not confidential" do
let(:scheme) { create(:scheme, sensitive: 0, owning_organisation:) }
it "exports every one of those columns as normal" do
expect(hidden_columns).not_to be_empty
hidden_columns.each do |attribute|
expect(csv_value(attribute)).not_to be_nil, "expected the #{attribute} column to be populated for a non-confidential-scheme log"
end
end
end
end
describe "the full CSV output" do describe "the full CSV output" do
context "when the requested log year is 2026" do context "when the requested log year is 2026" do
let(:year) { 2026 } let(:year) { 2026 }

Loading…
Cancel
Save