Browse Source

CLDC-4462: misc follow up actions

CLDC-4462-clear-confidential-data-follow-up-actions
Nat Dean-Lewis 1 day ago
parent
commit
17b356e744
  1. 37
      app/services/csv/lettings_log_csv_service.rb
  2. 119
      spec/models/lettings_log_spec.rb
  3. 55
      spec/services/csv/lettings_log_csv_service_spec.rb

37
app/services/csv/lettings_log_csv_service.rb

@ -280,32 +280,6 @@ 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)
@ -370,7 +344,6 @@ 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]
@ -404,16 +377,6 @@ 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

119
spec/models/lettings_log_spec.rb

@ -467,7 +467,7 @@ RSpec.describe LettingsLog do
describe "when changing a log's scheme and hence calling reset_scheme_location!" do describe "when changing a log's scheme and hence calling reset_scheme_location!" do
context "when there is one valid location and many invalid locations in the new scheme" do context "when there is one valid location and many invalid locations in the new scheme" do
let(:scheme) { create(:scheme) } let(:scheme) { create(:scheme, sensitive: 0) }
let(:invalid_location_1) { create(:location, scheme:, startdate: Time.zone.today + 3.weeks) } let(:invalid_location_1) { create(:location, scheme:, startdate: Time.zone.today + 3.weeks) }
let(:valid_location) { create(:location, scheme:, startdate: Time.zone.yesterday) } let(:valid_location) { create(:location, scheme:, startdate: Time.zone.yesterday) }
let(:invalid_location_2) { create(:location, scheme:, startdate: Time.zone.today + 3.weeks) } let(:invalid_location_2) { create(:location, scheme:, startdate: Time.zone.today + 3.weeks) }
@ -479,9 +479,9 @@ RSpec.describe LettingsLog do
end end
context "when there are many valid locations in the new scheme" do context "when there are many valid locations in the new scheme" do
let(:old_scheme) { create(:scheme, owning_organisation:) } let(:old_scheme) { create(:scheme, sensitive: 0, owning_organisation:) }
let(:old_location) { create(:location, scheme: old_scheme) } let(:old_location) { create(:location, scheme: old_scheme) }
let(:new_scheme) { create(:scheme, owning_organisation:) } let(:new_scheme) { create(:scheme, sensitive: 0, owning_organisation:) }
before do before do
create_list(:location, 2, scheme: new_scheme) create_list(:location, 2, scheme: new_scheme)
@ -508,7 +508,7 @@ RSpec.describe LettingsLog do
end end
context "and a scheme with a single log is selected" do context "and a scheme with a single log is selected" do
let(:scheme) { create(:scheme, owning_organisation:) } let(:scheme) { create(:scheme, sensitive: 0, owning_organisation:) }
let!(:location) { create(:location, scheme:) } let!(:location) { create(:location, scheme:) }
before do before do
@ -666,7 +666,7 @@ RSpec.describe LettingsLog do
end end
context "and not renewal" do context "and not renewal" do
let(:scheme) { create(:scheme, owning_organisation:) } let(:scheme) { create(:scheme, sensitive: 0, owning_organisation:) }
let(:location) { create(:location, scheme:, postcode: "M11AE", type_of_unit: 1, mobility_type: "W") } let(:location) { create(:location, scheme:, postcode: "M11AE", type_of_unit: 1, mobility_type: "W") }
let(:supported_housing_lettings_log) do let(:supported_housing_lettings_log) do
@ -1091,7 +1091,7 @@ RSpec.describe LettingsLog do
end end
describe "resetting invalidated fields" do describe "resetting invalidated fields" do
let(:scheme) { create(:scheme, owning_organisation: assigned_to_user.organisation) } let(:scheme) { create(:scheme, sensitive: 0, owning_organisation: assigned_to_user.organisation) }
let!(:location) { create(:location, location_code: "E07000223", scheme:) } let!(:location) { create(:location, location_code: "E07000223", scheme:) }
let(:lettings_log) do let(:lettings_log) do
create( create(
@ -1181,7 +1181,7 @@ RSpec.describe LettingsLog do
let(:organisation_2) { create(:organisation) } let(:organisation_2) { create(:organisation) }
context "when the organisation selected doesn't match the scheme set" do context "when the organisation selected doesn't match the scheme set" do
let(:scheme) { create(:scheme, owning_organisation: assigned_to_user.organisation) } let(:scheme) { create(:scheme, sensitive: 0, owning_organisation: assigned_to_user.organisation) }
let(:location) { create_list(:location, 2, scheme:).first } let(:location) { create_list(:location, 2, scheme:).first }
let(:lettings_log) { create(:lettings_log, owning_organisation: nil, needstype: 2, scheme_id: scheme.id, location_id: location.id) } let(:lettings_log) { create(:lettings_log, owning_organisation: nil, needstype: 2, scheme_id: scheme.id, location_id: location.id) }
@ -1194,7 +1194,7 @@ RSpec.describe LettingsLog do
end end
context "when the organisation selected still matches the scheme set" do context "when the organisation selected still matches the scheme set" do
let(:scheme) { create(:scheme, owning_organisation: organisation_2) } let(:scheme) { create(:scheme, sensitive: 0, owning_organisation: organisation_2) }
let(:location) { create_list(:location, 2, scheme:).first } let(:location) { create_list(:location, 2, scheme:).first }
let(:lettings_log) { create(:lettings_log, owning_organisation: nil, needstype: 2, scheme_id: scheme.id, location_id: location.id) } let(:lettings_log) { create(:lettings_log, owning_organisation: nil, needstype: 2, scheme_id: scheme.id, location_id: location.id) }
@ -1797,7 +1797,7 @@ RSpec.describe LettingsLog do
end end
context "when there is a duplicate supported housing log" do context "when there is a duplicate supported housing log" do
let(:scheme) { create(:scheme) } let(:scheme) { create(:scheme, sensitive: 0) }
let(:location) { create(:location, scheme:) } let(:location) { create(:location, scheme:) }
let(:location_2) { create(:location, scheme:) } let(:location_2) { create(:location, scheme:) }
let(:supported_housing_log) { create(:lettings_log, :duplicate, needstype: 2, location:, scheme:, owning_organisation: organisation) } let(:supported_housing_log) { create(:lettings_log, :duplicate, needstype: 2, location:, scheme:, owning_organisation: organisation) }
@ -1963,7 +1963,7 @@ RSpec.describe LettingsLog do
end end
context "when there is a duplicate supported housing log" do context "when there is a duplicate supported housing log" do
let(:scheme) { create(:scheme, owning_organisation: organisation) } let(:scheme) { create(:scheme, sensitive: 0, owning_organisation: organisation) }
let(:location) { create(:location, scheme:) } let(:location) { create(:location, scheme:) }
let!(:supported_housing_log) { create(:lettings_log, :duplicate, needstype: 2, location:, scheme:, owning_organisation: organisation) } let!(:supported_housing_log) { create(:lettings_log, :duplicate, needstype: 2, location:, scheme:, owning_organisation: organisation) }
let!(:duplicate_supported_housing_log) { create(:lettings_log, :duplicate, needstype: 2, location:, scheme:, owning_organisation: organisation) } let!(:duplicate_supported_housing_log) { create(:lettings_log, :duplicate, needstype: 2, location:, scheme:, owning_organisation: organisation) }
@ -2240,5 +2240,104 @@ RSpec.describe LettingsLog do
expect(log.scheme_has_confidential_information?).to be true expect(log.scheme_has_confidential_information?).to be true
end end
end end
describe "clearing confidential scheme address data on save" do
let(:owning_organisation) { create(:organisation) }
let(:scheme) { create(:scheme, sensitive: 1, owning_organisation:) }
let(:location) { create(:location, scheme:) }
let(:log) do
create(
:lettings_log,
:ignore_validation_errors,
needstype: 2,
owning_organisation:,
managing_organisation: owning_organisation,
scheme:,
location:,
startdate: Time.zone.local(2026, 5, 1),
)
end
it "clears address, UPRN and LA data collected before the confidential address feature" do
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_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",
address_search_value_check: 1,
la: "E09000003",
is_la_inferred: true,
)
log.skip_uprn_lookup = true
log.skip_address_lookup = true
log.valid?
log.save!(validate: false)
log.reload
expect(log.uprn).to be_nil
expect(log.uprn_known).to be_nil
expect(log.uprn_confirmed).to be_nil
expect(log.uprn_selection).to be_nil
expect(log.address_line1).to be_nil
expect(log.address_line2).to be_nil
expect(log.town_or_city).to be_nil
expect(log.county).to be_nil
expect(log.postcode_known).to be_nil
expect(log.address_line1_input).to be_nil
expect(log.postcode_full_input).to be_nil
expect(log.address_line1_as_entered).to be_nil
expect(log.address_line2_as_entered).to be_nil
expect(log.town_or_city_as_entered).to be_nil
expect(log.county_as_entered).to be_nil
expect(log.postcode_full_as_entered).to be_nil
expect(log.la_as_entered).to be_nil
expect(log.address_search_value_check).to be_nil
# la is deliberately left null in the DB - see LettingsLog#la, which derives it live
# from the scheme location on every read, and reset_and_infer_la! (which only records
# whether that fallback resolves, via is_la_inferred, without persisting the value)
expect(log[:la]).to be_nil
expect(log.la).to eq(location.location_code)
expect(log.is_la_inferred).to be true
end
it "still infers the local authority from the scheme's location after clearing" do
log.update_columns(la: "E09000003", is_la_inferred: false)
log.valid?
log.save!(validate: false)
expect(log.la).to eq(location.location_code)
expect(log[:la]).to be_nil
expect(log.is_la_inferred).to be true
end
it "leaves la blank and is_la_inferred false when the location has no resolvable local authority" do
location.update_columns(location_code: nil, location_admin_district: nil, is_la_inferred: false)
log.update_columns(la: "E09000003", is_la_inferred: false)
log.valid?
log.save!(validate: false)
expect(log.la).to be_nil
expect(log.is_la_inferred).to be false
end
it "does not affect the scheme or location" do
log.update_columns(address_line1: "1 Secret Street")
log.valid?
expect { log.save!(validate: false) }.not_to change(log, :scheme_id)
expect(log.location_id).to eq(location.id)
end
end
end end
# rubocop:enable RSpec/MessageChain # rubocop:enable RSpec/MessageChain

55
spec/services/csv/lettings_log_csv_service_spec.rb

@ -193,14 +193,31 @@ 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 describe "confidential scheme behaviour" do
let(:year) { 2026 } let(:year) { 2026 }
let(:owning_organisation) { create(:organisation) } let(:owning_organisation) { create(:organisation) }
let(:scheme) { create(:scheme, sensitive: 1, owning_organisation:) } let(:scheme) { create(:scheme, sensitive: 1, owning_organisation:) }
let(:location) { create(:location, scheme:) } let(:location) { create(:location, scheme:) }
# Every hidden field that actually appears as a column in this export. # Address/UPRN columns that have no location-based fallback, so must simply be nil.
let(:hidden_columns) { described_class::ADDRESS_FIELDS_HIDDEN_FOR_CONFIDENTIAL_SCHEME & attribute_line } let(:blanked_columns) do
%w[uprn
uprn_known
uprn_confirmed
uprn_selection
address_line1
address_line2
town_or_city
county
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] & attribute_line
end
let(:log) do let(:log) do
create( create(
:lettings_log, :lettings_log,
@ -214,8 +231,9 @@ RSpec.describe Csv::LettingsLogCsvService do
startdate: Time.zone.local(2026, 5, 1), startdate: Time.zone.local(2026, 5, 1),
).tap do |confidential_log| ).tap do |confidential_log|
# Simulate a log created before the confidential address feature that still holds # Simulate a log created before the confidential address feature that still holds
# property address data in the database. Populate every hidden address column so # property address data in the database, bypassing callbacks/validations. Saving it
# that the blanking is observable (a nil column would pass the assertion vacuously). # again (as any ordinary edit of the log would) must clear this legacy data via
# DerivedVariables::LettingsLogVariables#set_derived_fields!.
confidential_log.update_columns( confidential_log.update_columns(
uprn: "123456789012", uprn: "123456789012",
uprn_known: 1, uprn_known: 1,
@ -225,7 +243,6 @@ RSpec.describe Csv::LettingsLogCsvService do
address_line2: "Flat 2", address_line2: "Flat 2",
town_or_city: "Secretville", town_or_city: "Secretville",
county: "Secretshire", county: "Secretshire",
postcode_full: "AB1 2CD",
postcode_known: 1, postcode_known: 1,
address_line1_input: "1 Secret Street input", address_line1_input: "1 Secret Street input",
postcode_full_input: "AB1 2CD", postcode_full_input: "AB1 2CD",
@ -237,6 +254,10 @@ RSpec.describe Csv::LettingsLogCsvService do
la_as_entered: "la as entered", la_as_entered: "la as entered",
la: "E09000003", la: "E09000003",
) )
confidential_log.skip_uprn_lookup = true
confidential_log.skip_address_lookup = true
confidential_log.valid?
confidential_log.save!(validate: false)
end end
end end
@ -245,26 +266,16 @@ RSpec.describe Csv::LettingsLogCsvService do
end end
context "when a log's scheme is confidential" do context "when a log's scheme is confidential" do
it "blanks every hidden address and UPRN column" do it "blanks every hidden address and UPRN column with no location-based fallback" do
expect(hidden_columns).not_to be_empty expect(blanked_columns).not_to be_empty
hidden_columns.each do |attribute| blanked_columns.each do |attribute|
expect(csv_value(attribute)).to be_nil, "expected the #{attribute} column to be blank for a confidential-scheme log" expect(csv_value(attribute)).to be_nil, "expected the #{attribute} column to be blank for a confidential-scheme log"
end end
end end
it "still exports the local authority" do it "exports the local authority and postcode inferred from the scheme's location, not the log's own (cleared) values" do
expect(csv_value("la")).to eq("E09000003") expect(csv_value("la")).to eq(location.location_code)
end expect(csv_value("postcode_full")).to eq(location.postcode) if attribute_line.include?("postcode_full")
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 end
end end

Loading…
Cancel
Save