From ea8e76c3ca3d14a5903af5f3f0cf7fbb8a75a58a Mon Sep 17 00:00:00 2001 From: Nat Dean-Lewis Date: Wed, 19 Aug 2026 17:30:48 +0100 Subject: [PATCH 1/4] feat: wip clear confidential address data --- clear_confidential_address_data.rb | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 clear_confidential_address_data.rb diff --git a/clear_confidential_address_data.rb b/clear_confidential_address_data.rb new file mode 100644 index 000000000..c9700f7a6 --- /dev/null +++ b/clear_confidential_address_data.rb @@ -0,0 +1,42 @@ +ADDRESS_FIELDS_TO_REMOVE_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 + + is_la_inferred + la + address_search_value_check + ].freeze + + +scope = LettingsLog + .joins(:scheme) + .where(schemes: { sensitive: true }) + .where( + ADDRESS_FIELDS_TO_REMOVE_FOR_CONFIDENTIAL_SCHEME.map { |field| "#{LettingsLog.table_name}.#{field} IS NOT NULL" }.join(" OR ") + ) + +scope.find_each do |log| + log.reset_address_fields! + log.uprn_selection = nil + log.postcode_known = nil + + log.la = log.la + + log.save! +end From e63f2a46af44d33257f21c7e1b60219da7ae17e3 Mon Sep 17 00:00:00 2001 From: Nat Dean-Lewis Date: Mon, 7 Sep 2026 12:34:01 +0100 Subject: [PATCH 2/4] CLDC-3362: full rake task for clearing confidential address data --- clear_confidential_address_data.rb | 42 ----------- .../clear_confidential_address_data.rake | 75 +++++++++++++++++++ 2 files changed, 75 insertions(+), 42 deletions(-) delete mode 100644 clear_confidential_address_data.rb create mode 100644 lib/tasks/clear_confidential_address_data.rake diff --git a/clear_confidential_address_data.rb b/clear_confidential_address_data.rb deleted file mode 100644 index c9700f7a6..000000000 --- a/clear_confidential_address_data.rb +++ /dev/null @@ -1,42 +0,0 @@ -ADDRESS_FIELDS_TO_REMOVE_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 - - is_la_inferred - la - address_search_value_check - ].freeze - - -scope = LettingsLog - .joins(:scheme) - .where(schemes: { sensitive: true }) - .where( - ADDRESS_FIELDS_TO_REMOVE_FOR_CONFIDENTIAL_SCHEME.map { |field| "#{LettingsLog.table_name}.#{field} IS NOT NULL" }.join(" OR ") - ) - -scope.find_each do |log| - log.reset_address_fields! - log.uprn_selection = nil - log.postcode_known = nil - - log.la = log.la - - log.save! -end diff --git a/lib/tasks/clear_confidential_address_data.rake b/lib/tasks/clear_confidential_address_data.rake new file mode 100644 index 000000000..eff626728 --- /dev/null +++ b/lib/tasks/clear_confidential_address_data.rake @@ -0,0 +1,75 @@ +desc "Clears already-collected address/UPRN data for 2026/27 lettings logs in confidential (sensitive) schemes" +task clear_confidential_address_data: :environment do + year = 2026 + + fields_present_scope = %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 + address_search_value_check + la + ] + + scope = LettingsLog + .filter_by_year(year) + .joins(:scheme) + .where(schemes: { sensitive: "Yes" }) + .where(fields_present_scope.map { |field| "#{LettingsLog.table_name}.#{field} IS NOT NULL" }.join(" OR ")) + + puts "Found #{scope.count} confidential-scheme lettings logs in #{year}/#{year + 1} with address data to clear" + scope.find_each do |log| + puts "log_id=#{log.id} scheme_id=#{log.scheme_id} location_id=#{log.location_id} status=#{log.status} la=#{log.la} postcode_full=#{log.postcode_full}" + end + + updated = 0 + + scope.find_each do |log| + original_status = log.status + + log.uprn = nil + log.uprn_known = nil + log.uprn_confirmed = nil + log.uprn_selection = nil + log.address_line1 = nil + log.address_line2 = nil + log.town_or_city = nil + log.county = nil + log.postcode_full = nil + log.postcode_known = nil + log.address_line1_input = nil + log.postcode_full_input = nil + log.address_line1_as_entered = nil + log.address_line2_as_entered = nil + log.town_or_city_as_entered = nil + log.county_as_entered = nil + log.postcode_full_as_entered = nil + log.la_as_entered = nil + log.address_search_value_check = nil + log.la = nil + log.is_la_inferred = log.la.present? + + if log.save(validate: false) + updated += 1 + puts "log_id=#{log.id} status changed: #{original_status} -> #{log.status}" if log.status != original_status + else + Rails.logger.error "CLDC-4462: failed to clear address data for log #{log.id}: #{log.errors.full_messages.join(', ')}" + end + end + + puts "#{updated} logs updated" +end From 784b9cc17f8a9a4ac41d859ede23c1e05a39561d Mon Sep 17 00:00:00 2001 From: Nat Dean-Lewis Date: Mon, 7 Sep 2026 13:35:11 +0100 Subject: [PATCH 3/4] CLDC-3362: add rake spec --- .../clear_confidential_address_data_spec.rb | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 spec/lib/tasks/clear_confidential_address_data_spec.rb diff --git a/spec/lib/tasks/clear_confidential_address_data_spec.rb b/spec/lib/tasks/clear_confidential_address_data_spec.rb new file mode 100644 index 000000000..65639f0cd --- /dev/null +++ b/spec/lib/tasks/clear_confidential_address_data_spec.rb @@ -0,0 +1,141 @@ +require "rails_helper" +require "rake" + +RSpec.describe "clear_confidential_address_data" do + describe ":clear_confidential_address_data", type: :task do + subject(:task) { Rake::Task["clear_confidential_address_data"] } + + before do + Rake.application.rake_require("tasks/clear_confidential_address_data") + Rake::Task.define_task(:environment) + task.reenable + end + + let(:owning_organisation) { create(:organisation) } + let(:scheme) { create(:scheme, sensitive: "Yes", owning_organisation:) } + let(:non_confidential_scheme) { create(:scheme, sensitive: "No", owning_organisation:) } + let(:location) { create(:location, scheme:) } + + def create_log_with_address_data(scheme:, location:, startdate:) + log = create( + :lettings_log, + :completed, + :sh, + :ignore_validation_errors, + owning_organisation:, + managing_organisation: owning_organisation, + scheme:, + location:, + startdate:, + ) + # Bypass callbacks/validations to simulate a log that already holds property + # address/UPRN data collected before the confidential address feature existed. + log.update_columns( + uprn: "123456789012", + address_line1: "1 Secret Street", + town_or_city: "Secretville", + postcode_known: 1, + la: "E09000003", + ) + log + end + + context "when a confidential-scheme log in 2026/27 has address data" do + let!(:log) { create_log_with_address_data(scheme:, location:, startdate: Time.zone.local(2026, 5, 1)) } + + it "clears the address and UPRN fields" do + task.invoke + log.reload + + expect(log.address_line1).to be_nil + expect(log.town_or_city).to be_nil + expect(log.uprn).to be_nil + expect(log.postcode_known).to be_nil + end + + it "keeps the log's status unchanged when the location's LA can be inferred" do + task.invoke + expect(log.reload.status).to eq("completed") + end + + it "resolves the local authority from the scheme's location without persisting it" do + task.invoke + log.reload + + expect(log.la).to eq(location.location_code) + expect(log[:la]).to be_nil + expect(log.is_la_inferred).to be true + end + end + + context "when the scheme's location has no resolvable local authority" do + let!(:log) { create_log_with_address_data(scheme:, location:, startdate: Time.zone.local(2026, 5, 1)) } + + before { location.update_columns(location_code: nil, location_admin_district: nil, is_la_inferred: false) } + + it "clears the address fields and forces the log to in_progress" do + task.invoke + log.reload + + expect(log.address_line1).to be_nil + expect(log.la).to be_nil + expect(log.status).to eq("in_progress") + end + end + + context "when the confidential-scheme log is outside the 2026/27 collection year" do + let!(:log) { create_log_with_address_data(scheme:, location:, startdate: Time.zone.local(2025, 5, 1)) } + + it "does not clear the address fields" do + task.invoke + expect(log.reload.address_line1).to eq("1 Secret Street") + end + end + + context "when the log's scheme is not confidential" do + let!(:log) { create_log_with_address_data(scheme: non_confidential_scheme, location:, startdate: Time.zone.local(2026, 5, 1)) } + + it "does not clear the address fields" do + task.invoke + expect(log.reload.address_line1).to eq("1 Secret Street") + end + end + + context "when a confidential-scheme 2026/27 log has no address data left" do + 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 "is not touched" do + expect { task.invoke }.not_to(change { log.reload.updated_at }) + end + end + + it "does not affect the scheme or location associations" do + log = create_log_with_address_data(scheme:, location:, startdate: Time.zone.local(2026, 5, 1)) + + task.invoke + log.reload + + expect(log.scheme_id).to eq(scheme.id) + expect(log.location_id).to eq(location.id) + end + + it "is idempotent" do + log = create_log_with_address_data(scheme:, location:, startdate: Time.zone.local(2026, 5, 1)) + + task.invoke + task.reenable + expect { task.invoke }.not_to(change { log.reload.updated_at }) + end + end +end From 5b7d00e1d54d66c781550d8c343532132580b57c Mon Sep 17 00:00:00 2001 From: Nat Dean-Lewis Date: Mon, 7 Sep 2026 16:27:15 +0100 Subject: [PATCH 4/4] feat: route la question when not inferred for supported housing --- app/models/derived_variables/lettings_log_variables.rb | 10 +++++++--- .../form/lettings/pages/property_local_authority.rb | 1 + app/models/lettings_log.rb | 6 +++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index 230b2a1dc..1803638dd 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -176,9 +176,13 @@ module DerivedVariables::LettingsLogVariables if !form.start_year_2026_or_later? && is_supported_housing? reset_address_fields! - elsif form.start_year_2026_or_later? && location_changed? - reset_address_fields! - self.la = nil + elsif form.start_year_2026_or_later? + if location_changed? + reset_address_fields! + self.la = nil + end + + self.is_la_inferred = la.present? if is_supported_housing? && location && self[:la].blank? end if scheme_has_confidential_information? diff --git a/app/models/form/lettings/pages/property_local_authority.rb b/app/models/form/lettings/pages/property_local_authority.rb index 5656ec2d9..f552776a0 100644 --- a/app/models/form/lettings/pages/property_local_authority.rb +++ b/app/models/form/lettings/pages/property_local_authority.rb @@ -5,6 +5,7 @@ class Form::Lettings::Pages::PropertyLocalAuthority < ::Form::Page @depends_on = [ { "is_la_inferred" => false, "is_general_needs?" => true, "form.start_year_2025_or_later?" => false, "address_search_given?" => true }, { "is_la_inferred" => false, "is_general_needs?" => true, "form.start_year_2025_or_later?" => true }, + { "is_la_inferred" => false, "is_supported_housing?" => true, "form.start_year_2026_or_later?" => true }, ] end diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 1eb9078ff..f5c153f66 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -31,7 +31,7 @@ class LettingsLog < Log before_validation :process_postcode_changes!, if: :postcode_full_changed? before_validation :process_previous_postcode_changes!, if: :ppostcode_full_changed? before_validation :reset_invalidated_dependent_fields! - before_validation :reset_location_fields!, unless: :postcode_known? + before_validation :reset_location_fields!, unless: :postcode_known_or_la_derived_from_scheme_location? before_validation :reset_previous_location_fields!, unless: :previous_postcode_known? before_validation :set_derived_fields! before_validation :process_uprn_change!, if: :should_process_uprn_change? @@ -375,6 +375,10 @@ class LettingsLog < Log postcode_known == 1 end + def postcode_known_or_la_derived_from_scheme_location? + postcode_known? || (form&.start_year_2026_or_later? && is_supported_housing? && location.present?) + end + def previous_postcode_known? # 0: Yes ppcodenk&.zero?