From a27639457f0ed9fcc13076b44b56578cbfe1847e Mon Sep 17 00:00:00 2001 From: Mo Seedat Date: Wed, 28 Sep 2022 10:19:05 +0100 Subject: [PATCH] Switch to perform_later for non-dev --- app/models/lettings_log.rb | 7 +- ...ngs_logs_import_service_altered.rb.exclude | 912 ++++++++++++++++++ dump.rdb | Bin 162492 -> 215808 bytes 3 files changed, 916 insertions(+), 3 deletions(-) create mode 100644 app/services/imports/lettings_logs_import_service_altered.rb.exclude diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 272281df6..8e3f8c601 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -24,11 +24,12 @@ class LettingsLog < Log validates_with LettingsLogValidator before_validation :recalculate_start_year!, if: :startdate_changed? before_validation :reset_scheme_location!, if: :scheme_changed?, unless: :location_changed? - before_validation :process_postcode_changes!, if: :postcode_full_changed? - #SLOW: before_validation :process_previous_postcode_changes!, if: :ppostcode_full_changed? - #SLOW: before_validation :reset_invalidated_dependent_fields! + # Warning: Postcode checks require external service request - can be slow + 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_previous_location_fields!, unless: :previous_postcode_known? before_validation :set_derived_fields! diff --git a/app/services/imports/lettings_logs_import_service_altered.rb.exclude b/app/services/imports/lettings_logs_import_service_altered.rb.exclude new file mode 100644 index 000000000..c93d14d97 --- /dev/null +++ b/app/services/imports/lettings_logs_import_service_altered.rb.exclude @@ -0,0 +1,912 @@ +require 'securerandom' +require 'json' + +module Imports + class LettingsLogsImportService < ImportService + include Wisper::Publisher + + def initialize(storage_service, logger = Rails.logger) + super + + Wisper.subscribe(LettingsLogImportListener.new, prefix: :on) + end + + def create_logs(folder) + import_from(folder, :create_log) + if @logs_with_discrepancies.count.positive? + @logger.warn("The following lettings logs had status discrepancies: [#{@logs_with_discrepancies.join(', ')}]") + end + end + + def import_from(folder, create_method) + filenames = @storage_service.list_files(folder) + filenames.each do |filename| + file_io = @storage_service.get_file_io(filename) + xml_document = Nokogiri::XML(file_io) + send(create_method, xml_document) + rescue StandardError => e + @logger.error "#{e.class} in #{filename}: #{e.message}. Caller: #{e.backtrace.first}" + end + end + + def local_load(folder) + filenames = Dir["#{folder}/**/*.xml"] + puts "FILENAMES (#{filenames.size}): #{filenames}" + + @run_id = SecureRandom.uuid.to_s + logs_import = LogsImport.create!( + run_id: @run_id, + started_at: Time.zone.now, + total: filenames.size, + discrepancies: [], + filenames: filenames + ) + + redis = Redis.new + redis.set(@run_id, Marshal.dump(logs_import)) + broadcast(::Import::STARTED, @run_id) + + filenames.each do |filename| + puts "Loading filename: #{filename}" + Rack::MiniProfiler.step("Start Processing file #{filename}") do + # Generate background job to process file completely + xml_document = Nokogiri::XML(File.open(filename)) + + LettingsLogImportJob.perform_later(@run_id, xml_document.to_s) + #send(:create_log, xml_document) + end + rescue StandardError => e + @logger.error "#{e.class} in #{filename}: #{e.message}. Caller: #{e.backtrace.first}" + end + + if @logs_with_discrepancies.count.positive? + @logger.warn("The following lettings logs had status discrepancies: [#{@logs_with_discrepancies.join(', ')}]") + end + end + end + + + class LettingsLogsImportProcessor + FORM_NAME_INDEX = { + start_year: 0, + rent_type: 2, + needs_type: 3, + }.freeze + + GN_SH = { + general_needs: 1, + supported_housing: 2, + }.freeze + + SR_AR_IR = { + social_rent: 1, + affordable_rent: 2, + intermediate_rent: 3, + }.freeze + + # For providertype, values are reversed!!! + PRP_LA = { + private_registered_provider: 1, + local_authority: 2, + }.freeze + + IRPRODUCT = { + rent_to_buy: 1, + london_living_rent: 2, + other_intermediate_rent_product: 3, + }.freeze + + # These must match our form + RENT_TYPE = { + social_rent: 0, + affordable_rent: 1, + london_affordable_rent: 2, + rent_to_buy: 3, + london_living_rent: 4, + other_intermediate_rent_product: 5, + }.freeze + + attr_reader :xml_doc, :logs_with_discrepancies, :logs_overridden, :discrepancy, :old_id + + def initialize(xml_doc) + @xml_doc = xml_doc + @discrepancy = false + @old_id = '' + + @logs_with_discrepancies = Set.new + @logs_overridden = Set.new + + end + + + def create_log + attributes = {} + lettings_log = nil + previous_status = field_value(xml_doc, "meta", "status") + + Rack::MiniProfiler.step("Loading attributes") do + # Required fields for status complete or logic to work + # Note: order matters when we derive from previous values (attributes parameter) + attributes["startdate"] = compose_date(xml_doc, "DAY", "MONTH", "YEAR") + attributes["owning_organisation_id"] = find_organisation_id(xml_doc, "OWNINGORGID") + attributes["managing_organisation_id"] = find_organisation_id(xml_doc, "MANINGORGID") + attributes["joint"] = unsafe_string_as_integer(xml_doc, "joint") + attributes["startertenancy"] = unsafe_string_as_integer(xml_doc, "_2a") + attributes["tenancy"] = unsafe_string_as_integer(xml_doc, "Q2b") + attributes["tenancycode"] = string_or_nil(xml_doc, "_2bTenCode") + attributes["tenancyother"] = string_or_nil(xml_doc, "Q2ba") + attributes["tenancylength"] = safe_string_as_integer(xml_doc, "_2cYears") + attributes["needstype"] = needs_type(xml_doc) + attributes["lar"] = london_affordable_rent(xml_doc) + attributes["irproduct"] = unsafe_string_as_integer(xml_doc, "IRProduct") + attributes["irproduct_other"] = string_or_nil(xml_doc, "IRProductOther") + attributes["rent_type"] = rent_type(xml_doc, attributes["lar"], attributes["irproduct"]) + attributes["hhmemb"] = household_members(xml_doc, previous_status) + (1..8).each do |index| + attributes["age#{index}"] = safe_string_as_integer(xml_doc, "P#{index}Age") + attributes["age#{index}_known"] = age_known(xml_doc, index, attributes["hhmemb"]) + attributes["sex#{index}"] = sex(xml_doc, index) + attributes["ecstat#{index}"] = unsafe_string_as_integer(xml_doc, "P#{index}Eco") + end + (2..8).each do |index| + attributes["relat#{index}"] = relat(xml_doc, index) + attributes["details_known_#{index}"] = details_known(index, attributes) + end + attributes["ethnic"] = unsafe_string_as_integer(xml_doc, "P1Eth") + attributes["ethnic_group"] = ethnic_group(attributes["ethnic"]) + attributes["national"] = unsafe_string_as_integer(xml_doc, "P1Nat") + attributes["preg_occ"] = unsafe_string_as_integer(xml_doc, "Preg") + + attributes["armedforces"] = unsafe_string_as_integer(xml_doc, "ArmedF") + attributes["leftreg"] = unsafe_string_as_integer(xml_doc, "LeftAF") + attributes["reservist"] = unsafe_string_as_integer(xml_doc, "Inj") + + attributes["hb"] = unsafe_string_as_integer(xml_doc, "Q6Ben") + attributes["benefits"] = unsafe_string_as_integer(xml_doc, "Q7Ben") + attributes["earnings"] = safe_string_as_decimal(xml_doc, "Q8Money") + attributes["net_income_known"] = net_income_known(xml_doc, attributes["earnings"]) + attributes["incfreq"] = unsafe_string_as_integer(xml_doc, "Q8a") + + attributes["reason"] = unsafe_string_as_integer(xml_doc, "Q9a") + attributes["reasonother"] = string_or_nil(xml_doc, "Q9aa") + attributes["underoccupation_benefitcap"] = unsafe_string_as_integer(xml_doc, "_9b") + %w[a b c f g h].each do |letter| + attributes["housingneeds_#{letter}"] = housing_needs(xml_doc, letter) + end + attributes["housingneeds"] = 1 if [attributes["housingneeds_a"], attributes["housingneeds_b"], attributes["housingneeds_c"], attributes["housingneeds_f"]].any? { |housingneed| housingneed == 1 } + attributes["housingneeds"] = 2 if attributes["housingneeds_g"] == 1 + attributes["housingneeds"] = 3 if attributes["housingneeds_h"] == 1 + attributes["housingneeds_type"] = 0 if attributes["housingneeds_a"] == 1 + attributes["housingneeds_type"] = 1 if attributes["housingneeds_b"] == 1 + attributes["housingneeds_type"] = 2 if attributes["housingneeds_c"] == 1 + attributes["housingneeds_type"] = 3 if attributes["housingneeds_f"] == 1 && [attributes["housingneeds_a"], attributes["housingneeds_b"], attributes["housingneeds_c"]].all? { |housingneed| housingneed != 1 } + attributes["housingneeds_other"] = attributes["housingneeds_f"] == 1 ? 1 : 0 + + attributes["illness"] = unsafe_string_as_integer(xml_doc, "Q10ia") + (1..10).each do |index| + attributes["illness_type_#{index}"] = illness_type(xml_doc, index, attributes["illness"]) + end + attributes["illness_type_0"] = 1 if (1..10).all? { |idx| attributes["illness_type_#{idx}"].nil? || attributes["illness_type_#{idx}"].zero? } + + attributes["prevten"] = unsafe_string_as_integer(xml_doc, "Q11") + attributes["prevloc"] = string_or_nil(xml_doc, "Q12aONS") + attributes["ppostcode_full"] = compose_postcode(xml_doc, "PPOSTC1", "PPOSTC2") + attributes["ppcodenk"] = previous_postcode_known(xml_doc, attributes["ppostcode_full"], attributes["prevloc"]) + attributes["layear"] = unsafe_string_as_integer(xml_doc, "Q12c") + attributes["waityear"] = unsafe_string_as_integer(xml_doc, "Q12d") + attributes["homeless"] = unsafe_string_as_integer(xml_doc, "Q13") + + attributes["reasonpref"] = unsafe_string_as_integer(xml_doc, "Q14a") + attributes["rp_homeless"] = unsafe_string_as_integer(xml_doc, "Q14b1").present? ? 1 : nil + attributes["rp_insan_unsat"] = unsafe_string_as_integer(xml_doc, "Q14b2").present? ? 1 : nil + attributes["rp_medwel"] = unsafe_string_as_integer(xml_doc, "Q14b3").present? ? 1 : nil + attributes["rp_hardship"] = unsafe_string_as_integer(xml_doc, "Q14b4").present? ? 1 : nil + attributes["rp_dontknow"] = unsafe_string_as_integer(xml_doc, "Q14b5").present? ? 1 : nil + + attributes["cbl"] = allocation_system(unsafe_string_as_integer(xml_doc, "Q15CBL")) + attributes["chr"] = allocation_system(unsafe_string_as_integer(xml_doc, "Q15CHR")) + attributes["cap"] = allocation_system(unsafe_string_as_integer(xml_doc, "Q15CAP")) + attributes["letting_allocation_unknown"] = allocation_system_unknown(attributes["cbl"], attributes["chr"], attributes["cap"]) + + attributes["referral"] = unsafe_string_as_integer(xml_doc, "Q16") + attributes["period"] = unsafe_string_as_integer(xml_doc, "Q17") + + attributes["brent"] = safe_string_as_decimal(xml_doc, "Q18ai") + attributes["scharge"] = safe_string_as_decimal(xml_doc, "Q18aii") + attributes["pscharge"] = safe_string_as_decimal(xml_doc, "Q18aiii") + attributes["supcharg"] = safe_string_as_decimal(xml_doc, "Q18aiv") + attributes["tcharge"] = safe_string_as_decimal(xml_doc, "Q18av") + + attributes["hbrentshortfall"] = unsafe_string_as_integer(xml_doc, "Q18d") + attributes["tshortfall"] = safe_string_as_decimal(xml_doc, "Q18dyes") + attributes["tshortfall_known"] = tshortfall_known?(xml_doc, attributes) + + attributes["voiddate"] = compose_date(xml_doc, "VDAY", "VMONTH", "VYEAR") + attributes["mrcdate"] = compose_date(xml_doc, "MRCDAY", "MRCMONTH", "MRCYEAR") + attributes["majorrepairs"] = if attributes["mrcdate"].present? && previous_status.include?("submitted") + 1 + elsif previous_status.include?("submitted") + 0 + end + + attributes["offered"] = safe_string_as_integer(xml_doc, "Q20") + attributes["propcode"] = string_or_nil(xml_doc, "Q21a") + attributes["beds"] = safe_string_as_integer(xml_doc, "Q22") + attributes["unittype_gn"] = unsafe_string_as_integer(xml_doc, "Q23") + attributes["builtype"] = unsafe_string_as_integer(xml_doc, "Q24") + attributes["wchair"] = unsafe_string_as_integer(xml_doc, "Q25") + attributes["unitletas"] = unsafe_string_as_integer(xml_doc, "Q26") + attributes["rsnvac"] = unsafe_string_as_integer(xml_doc, "Q27") + attributes["renewal"] = renewal(attributes["rsnvac"]) + + attributes["la"] = string_or_nil(xml_doc, "Q28ONS") + attributes["postcode_full"] = compose_postcode(xml_doc, "POSTCODE", "POSTCOD2") + attributes["postcode_known"] = postcode_known(attributes) + + # Not specific to our form but required for consistency (present in import) + attributes["old_form_id"] = safe_string_as_integer(xml_doc, "FORM") + attributes["created_at"] = Time.zone.parse(field_value(xml_doc, "meta", "created-date")) + attributes["updated_at"] = Time.zone.parse(field_value(xml_doc, "meta", "modified-date")) + attributes["old_id"] = field_value(xml_doc, "meta", "document-id") + @old_id = attributes["old_id"] + + # Required for our form invalidated questions (not present in import) + attributes["previous_la_known"] = attributes["prevloc"].nil? ? 0 : 1 + attributes["is_la_inferred"] = attributes["postcode_full"].present? + attributes["first_time_property_let_as_social_housing"] = first_time_let(attributes["rsnvac"]) + attributes["declaration"] = declaration(xml_doc) + + set_partial_charges_to_zero(attributes) + + # Supported Housing fields + if attributes["needstype"] == GN_SH[:supported_housing] + location_old_visible_id = safe_string_as_integer(xml_doc, "_1cschemecode") + scheme_old_visible_id = safe_string_as_integer(xml_doc, "_1cmangroupcode") + + schemes = Scheme.where(old_visible_id: scheme_old_visible_id, owning_organisation_id: attributes["owning_organisation_id"]) + location = Location.find_by(old_visible_id: location_old_visible_id, scheme: schemes) + raise "No matching location for scheme #{scheme_old_visible_id} and location #{location_old_visible_id} (visible IDs)" if location.nil? + + # Set the scheme via location, because the scheme old visible ID can be duplicated at import + attributes["location_id"] = location.id + attributes["scheme_id"] = location.scheme.id + attributes["sheltered"] = unsafe_string_as_integer(xml_doc, "Q1e") + attributes["chcharge"] = safe_string_as_decimal(xml_doc, "Q18b") + attributes["household_charge"] = household_charge(xml_doc) + attributes["is_carehome"] = is_carehome(location.scheme) + end + + # Handles confidential schemes + if attributes["postcode_full"] == "******" + attributes["postcode_known"] = 0 + attributes["postcode_full"] = nil + end + + # Soft validations can become required answers, set them to yes by default + attributes["pregnancy_value_check"] = 0 + attributes["major_repairs_date_value_check"] = 0 + attributes["void_date_value_check"] = 0 + attributes["retirement_value_check"] = 0 + attributes["rent_value_check"] = 0 + attributes["net_income_value_check"] = 0 + + # Sets the log creator + owner_id = field_value(xml_doc, "meta", "owner-user-id").strip + if owner_id.present? + attributes["created_by"] = User.find_by(old_user_id: owner_id) + end + + apply_date_consistency!(attributes) + apply_household_consistency!(attributes) + end # ENDPROFILER + + + lettings_log = save_lettings_log(attributes) + + compute_differences(lettings_log, attributes) + check_status_completed(lettings_log, previous_status) unless @logs_overridden.include?(lettings_log.old_id) + + end + + + + (2..8).each do |index| + attributes["relat#{index}"] = relat(xml_doc, index) + attributes["details_known_#{index}"] = details_known(index, attributes) + + # Trips validation + if attributes["age#{index}"].present? && attributes["age#{index}"] < 16 && attributes["relat#{index}"].present? && attributes["relat#{index}"] != "C" && attributes["relat#{index}"] != "R" + attributes["age#{index}"] = nil + attributes["relat#{index}"] = nil + end + end + attributes["ethnic"] = unsafe_string_as_integer(xml_doc, "P1Eth") + attributes["ethnic_group"] = ethnic_group(attributes["ethnic"]) + attributes["national"] = unsafe_string_as_integer(xml_doc, "P1Nat") + attributes["preg_occ"] = unsafe_string_as_integer(xml_doc, "Preg") + + attributes["armedforces"] = unsafe_string_as_integer(xml_doc, "ArmedF") + attributes["leftreg"] = unsafe_string_as_integer(xml_doc, "LeftAF") + attributes["reservist"] = unsafe_string_as_integer(xml_doc, "Inj") + + attributes["hb"] = unsafe_string_as_integer(xml_doc, "Q6Ben") + attributes["benefits"] = unsafe_string_as_integer(xml_doc, "Q7Ben") + attributes["earnings"] = safe_string_as_decimal(xml_doc, "Q8Money") + attributes["net_income_known"] = net_income_known(xml_doc, attributes["earnings"]) + attributes["incfreq"] = unsafe_string_as_integer(xml_doc, "Q8a") + + attributes["reason"] = unsafe_string_as_integer(xml_doc, "Q9a") + attributes["reasonother"] = string_or_nil(xml_doc, "Q9aa") + attributes["underoccupation_benefitcap"] = unsafe_string_as_integer(xml_doc, "_9b") + %w[a b c f g h].each do |letter| + attributes["housingneeds_#{letter}"] = housing_needs(xml_doc, letter) + end + attributes["housingneeds"] = 1 if [attributes["housingneeds_a"], attributes["housingneeds_b"], attributes["housingneeds_c"], attributes["housingneeds_f"]].any? { |housingneed| housingneed == 1 } + attributes["housingneeds"] = 2 if attributes["housingneeds_g"] == 1 + attributes["housingneeds"] = 3 if attributes["housingneeds_h"] == 1 + attributes["housingneeds_type"] = 0 if attributes["housingneeds_a"] == 1 + attributes["housingneeds_type"] = 1 if attributes["housingneeds_b"] == 1 + attributes["housingneeds_type"] = 2 if attributes["housingneeds_c"] == 1 + attributes["housingneeds_type"] = 3 if attributes["housingneeds_f"] == 1 && [attributes["housingneeds_a"], attributes["housingneeds_b"], attributes["housingneeds_c"]].all? { |housingneed| housingneed != 1 } + attributes["housingneeds_other"] = attributes["housingneeds_f"] == 1 ? 1 : 0 + + attributes["illness"] = unsafe_string_as_integer(xml_doc, "Q10ia") + (1..10).each do |index| + attributes["illness_type_#{index}"] = illness_type(xml_doc, index, attributes["illness"]) + end + attributes["illness_type_0"] = 1 if (1..10).all? { |idx| attributes["illness_type_#{idx}"].nil? || attributes["illness_type_#{idx}"].zero? } + + attributes["prevten"] = unsafe_string_as_integer(xml_doc, "Q11") + attributes["prevloc"] = string_or_nil(xml_doc, "Q12aONS") + attributes["ppostcode_full"] = compose_postcode(xml_doc, "PPOSTC1", "PPOSTC2") + attributes["ppcodenk"] = previous_postcode_known(xml_doc, attributes["ppostcode_full"], attributes["prevloc"]) + attributes["layear"] = unsafe_string_as_integer(xml_doc, "Q12c") + attributes["waityear"] = unsafe_string_as_integer(xml_doc, "Q12d") + attributes["homeless"] = unsafe_string_as_integer(xml_doc, "Q13") + + attributes["reasonpref"] = unsafe_string_as_integer(xml_doc, "Q14a") + attributes["rp_homeless"] = unsafe_string_as_integer(xml_doc, "Q14b1").present? ? 1 : nil + attributes["rp_insan_unsat"] = unsafe_string_as_integer(xml_doc, "Q14b2").present? ? 1 : nil + attributes["rp_medwel"] = unsafe_string_as_integer(xml_doc, "Q14b3").present? ? 1 : nil + attributes["rp_hardship"] = unsafe_string_as_integer(xml_doc, "Q14b4").present? ? 1 : nil + attributes["rp_dontknow"] = unsafe_string_as_integer(xml_doc, "Q14b5").present? ? 1 : nil + + # Trips validation + if attributes["homeless"] == 1 && attributes["rp_homeless"] == 1 + attributes["homeless"] = nil + attributes["rp_homeless"] = nil + end + + attributes["cbl"] = allocation_system(unsafe_string_as_integer(xml_doc, "Q15CBL")) + attributes["chr"] = allocation_system(unsafe_string_as_integer(xml_doc, "Q15CHR")) + attributes["cap"] = allocation_system(unsafe_string_as_integer(xml_doc, "Q15CAP")) + attributes["letting_allocation_unknown"] = allocation_system_unknown(attributes["cbl"], attributes["chr"], attributes["cap"]) + + attributes["referral"] = unsafe_string_as_integer(xml_doc, "Q16") + attributes["period"] = unsafe_string_as_integer(xml_doc, "Q17") + + attributes["brent"] = safe_string_as_decimal(xml_doc, "Q18ai") + attributes["scharge"] = safe_string_as_decimal(xml_doc, "Q18aii") + attributes["pscharge"] = safe_string_as_decimal(xml_doc, "Q18aiii") + attributes["supcharg"] = safe_string_as_decimal(xml_doc, "Q18aiv") + attributes["tcharge"] = safe_string_as_decimal(xml_doc, "Q18av") + + attributes["hbrentshortfall"] = unsafe_string_as_integer(xml_doc, "Q18d") + attributes["tshortfall"] = safe_string_as_decimal(xml_doc, "Q18dyes") + attributes["tshortfall_known"] = tshortfall_known?(xml_doc, attributes) + + attributes["voiddate"] = compose_date(xml_doc, "VDAY", "VMONTH", "VYEAR") + attributes["mrcdate"] = compose_date(xml_doc, "MRCDAY", "MRCMONTH", "MRCYEAR") + attributes["majorrepairs"] = if attributes["mrcdate"].present? && previous_status.include?("submitted") + 1 + elsif previous_status.include?("submitted") + 0 + end + + attributes["offered"] = safe_string_as_integer(xml_doc, "Q20") + attributes["propcode"] = string_or_nil(xml_doc, "Q21a") + attributes["beds"] = safe_string_as_integer(xml_doc, "Q22") + attributes["unittype_gn"] = unsafe_string_as_integer(xml_doc, "Q23") + attributes["builtype"] = unsafe_string_as_integer(xml_doc, "Q24") + attributes["wchair"] = unsafe_string_as_integer(xml_doc, "Q25") + attributes["unitletas"] = unsafe_string_as_integer(xml_doc, "Q26") + attributes["rsnvac"] = unsafe_string_as_integer(xml_doc, "Q27") + attributes["renewal"] = renewal(attributes["rsnvac"]) + + attributes["la"] = string_or_nil(xml_doc, "Q28ONS") + attributes["postcode_full"] = compose_postcode(xml_doc, "POSTCODE", "POSTCOD2") + attributes["postcode_known"] = postcode_known(attributes) + + # Not specific to our form but required for consistency (present in import) + attributes["old_form_id"] = safe_string_as_integer(xml_doc, "FORM") + attributes["created_at"] = Time.zone.parse(field_value(xml_doc, "meta", "created-date")) + attributes["updated_at"] = Time.zone.parse(field_value(xml_doc, "meta", "modified-date")) + attributes["old_id"] = field_value(xml_doc, "meta", "document-id") + + # Required for our form invalidated questions (not present in import) + attributes["previous_la_known"] = attributes["prevloc"].nil? ? 0 : 1 + attributes["is_la_inferred"] = attributes["postcode_full"].present? + attributes["first_time_property_let_as_social_housing"] = first_time_let(attributes["rsnvac"]) + attributes["declaration"] = declaration(xml_doc) + + set_partial_charges_to_zero(attributes) + + # Supported Housing fields + if attributes["needstype"] == GN_SH[:supported_housing] + location_old_visible_id = safe_string_as_integer(xml_doc, "_1cschemecode") + scheme_old_visible_id = safe_string_as_integer(xml_doc, "_1cmangroupcode") + + schemes = Scheme.where(old_visible_id: scheme_old_visible_id, owning_organisation_id: attributes["owning_organisation_id"]) + location = Location.find_by(old_visible_id: location_old_visible_id, scheme: schemes) + raise "No matching location for scheme #{scheme_old_visible_id} and location #{location_old_visible_id} (visible IDs)" if location.nil? + + # Set the scheme via location, because the scheme old visible ID can be duplicated at import + attributes["location_id"] = location.id + attributes["scheme_id"] = location.scheme.id + attributes["sheltered"] = unsafe_string_as_integer(xml_doc, "Q1e") + attributes["chcharge"] = safe_string_as_decimal(xml_doc, "Q18b") + attributes["household_charge"] = household_charge(xml_doc) + attributes["is_carehome"] = is_carehome(location.scheme) + end + + # Handles confidential schemes + if attributes["postcode_full"] == "******" + attributes["postcode_known"] = 0 + attributes["postcode_full"] = nil + end + + # Soft validations can become required answers, set them to yes by default + attributes["pregnancy_value_check"] = 0 + attributes["major_repairs_date_value_check"] = 0 + attributes["void_date_value_check"] = 0 + attributes["retirement_value_check"] = 0 + attributes["rent_value_check"] = 0 + attributes["net_income_value_check"] = 0 + + # Sets the log creator + owner_id = field_value(xml_doc, "meta", "owner-user-id").strip + if owner_id.present? + user = LegacyUser.find_by(old_user_id: owner_id)&.user + @logger.warn "Missing user! We expected to find a legacy user with old_user_id #{owner_id}" unless user + + attributes["created_by"] = user + end + + apply_date_consistency!(attributes) + apply_household_consistency!(attributes) + + lettings_log = save_lettings_log(attributes) + compute_differences(lettings_log, attributes) + check_status_completed(lettings_log, previous_status) unless @logs_overridden.include?(lettings_log.old_id) + end + + def save_lettings_log(attributes) + lettings_log = LettingsLog.new(attributes) + begin + lettings_log.save! + lettings_log + rescue ActiveRecord::RecordNotUnique + legacy_id = attributes["old_id"] + record = LettingsLog.find_by(old_id: legacy_id) + @logger.info "Updating lettings log #{record.id} with legacy ID #{legacy_id}" + record.update!(attributes) + record + rescue ActiveRecord::RecordInvalid => e + rescue_validation_or_raise(lettings_log, attributes, e) + end + end + + def rescue_validation_or_raise(lettings_log, attributes, exception) + if lettings_log.errors.of_kind?(:referral, :internal_transfer_non_social_housing) + @logger.warn("Log #{lettings_log.old_id}: Removing internal transfer referral since previous tenancy is a non social housing") + @logs_overridden << lettings_log.old_id + attributes.delete("referral") + save_lettings_log(attributes) + elsif lettings_log.errors.of_kind?(:referral, :internal_transfer_fixed_or_lifetime) + @logger.warn("Log #{lettings_log.old_id}: Removing internal transfer referral since previous tenancy is fixed terms or lifetime") + @logs_overridden << lettings_log.old_id + attributes.delete("referral") + save_lettings_log(attributes) + else + raise exception + end + end + + def compute_differences(lettings_log, attributes) + differences = [] + attributes.each do |key, value| + lettings_log_value = lettings_log.send(key.to_sym) + next if fields_not_present_in_softwire_data.include?(key) + + if value != lettings_log_value + differences.push("#{key} #{value.inspect} #{lettings_log_value.inspect}") + end + end + @logger.warn "Differences found when saving log #{lettings_log.old_id}: #{differences}" unless differences.empty? + end + + def fields_not_present_in_softwire_data + %w[majorrepairs illness_type_0 tshortfall_known pregnancy_value_check retirement_value_check rent_value_check net_income_value_check major_repairs_date_value_check void_date_value_check housingneeds_type housingneeds_other] + end + + def check_status_completed(lettings_log, previous_status) + if previous_status.include?("submitted") && lettings_log.status != "completed" + @logger.warn "lettings log #{lettings_log.id} is not completed" + @logger.warn "lettings log with old id:#{lettings_log.old_id} is incomplete but status should be complete" + @logs_with_discrepancies << lettings_log.old_id + @discrepancy = true + end + end + + # Safe: A string that represents only an integer (or empty/nil) + def safe_string_as_integer(xml_doc, attribute) + str = field_value(xml_doc, "xmlns", attribute) + Integer(str, exception: false) + end + + # Safe: A string that represents only a decimal (or empty/nil) + def safe_string_as_decimal(xml_doc, attribute) + str = string_or_nil(xml_doc, attribute) + if str.nil? + nil + else + BigDecimal(str, exception: false) + end + end + + # Unsafe: A string that has more than just the integer value + def unsafe_string_as_integer(xml_doc, attribute) + str = string_or_nil(xml_doc, attribute) + if str.nil? + nil + else + str.to_i + end + end + + def compose_date(xml_doc, day_str, month_str, year_str) + day = Integer(field_value(xml_doc, "xmlns", day_str), exception: false) + month = Integer(field_value(xml_doc, "xmlns", month_str), exception: false) + year = Integer(field_value(xml_doc, "xmlns", year_str), exception: false) + if day.nil? || month.nil? || year.nil? + nil + else + Time.zone.local(year, month, day) + end + end + + def get_form_name_component(xml_doc, index) + form_name = field_value(xml_doc, "meta", "form-name") + form_type_components = form_name.split("-") + form_type_components[index] + end + + def needs_type(xml_doc) + gn_sh = get_form_name_component(xml_doc, FORM_NAME_INDEX[:needs_type]) + case gn_sh + when "GN" + GN_SH[:general_needs] + when "SH" + GN_SH[:supported_housing] + else + raise "Unknown needstype value: #{gn_sh}" + end + end + + # This does not match renttype (CDS) which is derived by lettings log logic + def rent_type(xml_doc, lar, irproduct) + sr_ar_ir = get_form_name_component(xml_doc, FORM_NAME_INDEX[:rent_type]) + + case sr_ar_ir + when "SR" + RENT_TYPE[:social_rent] + when "AR" + if lar == 1 + RENT_TYPE[:london_affordable_rent] + else + RENT_TYPE[:affordable_rent] + end + when "IR" + if irproduct == IRPRODUCT[:rent_to_buy] + RENT_TYPE[:rent_to_buy] + elsif irproduct == IRPRODUCT[:london_living_rent] + RENT_TYPE[:london_living_rent] + elsif irproduct == IRPRODUCT[:other_intermediate_rent_product] + RENT_TYPE[:other_intermediate_rent_product] + end + else + raise "Could not infer rent type with '#{sr_ar_ir}'" + end + end + + def find_organisation_id(xml_doc, id_field) + old_visible_id = unsafe_string_as_integer(xml_doc, id_field) + organisation = Organisation.find_by(old_visible_id:) + raise "Organisation not found with legacy ID #{old_visible_id}" if organisation.nil? + + organisation.id + end + + def sex(xml_doc, index) + sex = string_or_nil(xml_doc, "P#{index}Sex") + case sex + when "Male" + "M" + when "Female" + "F" + when "Other", "Non-binary" + "X" + when "Refused" + "R" + end + end + + def relat(xml_doc, index) + relat = string_or_nil(xml_doc, "P#{index}Rel") + case relat + when "Child" + "C" + when "Partner" + "P" + when "Other", "Non-binary" + "X" + when "Refused" + "R" + end + end + + def age_known(xml_doc, index, hhmemb) + return nil if hhmemb.present? && index > hhmemb + + age_refused = string_or_nil(xml_doc, "P#{index}AR") + if age_refused.present? + if age_refused.casecmp?("AGE_REFUSED") || age_refused.casecmp?("No") + return 1 # No + else + return 0 # Yes + end + end + 0 + end + + def details_known(index, attributes) + return nil if attributes["hhmemb"].nil? || index > attributes["hhmemb"] + + if attributes["age#{index}_known"] == 1 && + attributes["sex#{index}"] == "R" && + attributes["relat#{index}"] == "R" && + attributes["ecstat#{index}"] == 10 + 1 # No + else + 0 # Yes + end + end + + def previous_postcode_known(xml_doc, previous_postcode, prevloc) + previous_postcode_known = string_or_nil(xml_doc, "Q12bnot") + if previous_postcode_known == "Temporary_or_Unknown" || (previous_postcode.nil? && prevloc.present?) + 0 + elsif previous_postcode.nil? + nil + else + 1 + end + end + + def compose_postcode(xml_doc, outcode, incode) + outcode_value = string_or_nil(xml_doc, outcode) + incode_value = string_or_nil(xml_doc, incode) + if outcode_value.nil? || incode_value.nil? || !"#{outcode_value} #{incode_value}".match(POSTCODE_REGEXP) + nil + else + "#{outcode_value} #{incode_value}" + end + end + + def london_affordable_rent(xml_doc) + lar = unsafe_string_as_integer(xml_doc, "LAR") + if lar == 1 + 1 + else + # We default to No for any other values (nil, not known) + 2 + end + end + + def renewal(rsnvac) + # Relet – renewal of fixed-term tenancy + if rsnvac == 14 + 1 + else + 0 + end + end + + def string_or_nil(xml_doc, attribute) + str = field_value(xml_doc, "xmlns", attribute) + str.presence + end + + def ethnic_group(ethnic) + case ethnic + when 1, 2, 3, 18 + # White + 0 + when 4, 5, 6, 7 + # Mixed + 1 + when 8, 9, 10, 11, 15 + # Asian + 2 + when 12, 13, 14 + # Black + 3 + when 16, 19 + # Others + 4 + when 17 + # Refused + 17 + end + end + + # Letters should be lowercase to match case + def housing_needs(xml_doc, letter) + housing_need = string_or_nil(xml_doc, "Q10-#{letter}") + if housing_need == "Yes" + 1 + else + 0 + end + end + + def net_income_known(xml_doc, earnings) + incref = string_or_nil(xml_doc, "Q8Refused") + if incref == "Refused" + # Tenant prefers not to say + 2 + elsif earnings.nil? + # No + 1 + else + # Yes + 0 + end + end + + def illness_type(xml_doc, index, illness) + illness_type = string_or_nil(xml_doc, "Q10ib-#{index}") + if illness_type == "Yes" && illness == 1 + 1 + elsif illness == 1 + 0 + end + end + + def first_time_let(rsnvac) + if [15, 16, 17].include?(rsnvac) + 1 + else + 0 + end + end + + def declaration(xml_doc) + declaration = string_or_nil(xml_doc, "Qdp") + if declaration == "Yes" + 1 + end + end + + def postcode_known(attributes) + if attributes["postcode_full"].nil? && attributes["la"].nil? + nil + elsif attributes["postcode_full"].nil? + 0 # Assumes we selected No in the form since the LA is present + else + 1 + end + end + + def household_members(xml_doc, previous_status) + hhmemb = safe_string_as_integer(xml_doc, "HHMEMB") + if previous_status.include?("submitted") && hhmemb.nil? + hhmemb = people_with_details(xml_doc).count + return nil if hhmemb.zero? + end + hhmemb + end + + def people_with_details(xml_doc) + ((2..8).map { |x| string_or_nil(xml_doc, "P#{x}Rel") } + [string_or_nil(xml_doc, "P1Sex")]).compact + end + + def tshortfall_known?(xml_doc, attributes) + if attributes["tshortfall"].blank? && attributes["hbrentshortfall"] == 1 && overridden?(xml_doc, "xmlns", "Q18dyes") + 1 + else + 0 + end + end + + def allocation_system(value) + case value + when 1 + 1 + when 2 + 0 + end + end + + def allocation_system_unknown(cbl, chr, cap) + allocation_values = [cbl, chr, cap] + if allocation_values.all?(&:nil?) + nil + elsif allocation_values.all? { |att| att&.zero? } + 1 + else + 0 + end + end + + def apply_date_consistency!(attributes) + return if attributes["voiddate"].nil? || attributes["startdate"].nil? + + if attributes["voiddate"] > attributes["startdate"] + attributes.delete("voiddate") + end + end + + def apply_household_consistency!(attributes) + (2..8).each do |index| + next if attributes["age#{index}"].nil? + + if attributes["age#{index}"] < 16 && attributes["relat#{index}"] == "R" + attributes["relat#{index}"] = "C" + end + end + end + + def household_charge(xml_doc) + value = string_or_nil(xml_doc, "Q18c") + start_year = Integer(get_form_name_component(xml_doc, FORM_NAME_INDEX[:start_year])) + + if start_year <= 2021 + # Yes means that there are no charges (2021 or earlier) + value && value.include?("Yes") ? 1 : 0 + else + # Yes means that there are charges (2022 onwards) + value && value.include?("Yes") ? 0 : 1 + end + end + + def set_partial_charges_to_zero(attributes) + unless attributes["brent"].nil? && + attributes["scharge"].nil? && + attributes["pscharge"].nil? && + attributes["supcharg"].nil? + attributes["brent"] ||= BigDecimal("0.0") + attributes["scharge"] ||= BigDecimal("0.0") + attributes["pscharge"] ||= BigDecimal("0.0") + attributes["supcharg"] ||= BigDecimal("0.0") + end + end + + def is_carehome(scheme) + return nil unless scheme + + if [2, 3, 4].include?(scheme.registered_under_care_act_before_type_cast) + 1 + else + 0 + end + end + + def discrepancy? + !!@discrepancy + end + end + + + +end diff --git a/dump.rdb b/dump.rdb index cfee83a42a1bdc79e7f0cff5893cfc2da738fea9..815f46a681f21da4d27bc82b9a6ad3eb56c9613b 100644 GIT binary patch delta 52220 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 IoOAyF0giv>RsaA1 delta 77 zcmV-T0J8spmJPi136L-+%nvkU`UrJ%Wn?XFWo^Q+3OE4z3}J6(EpT#WVQpe;WxxRb j0Q(w|S3Hv~(i?;F(1-HS0k`td0tR{r61$<&TFRbE^i3i?