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? 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 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