diff --git a/Gemfile.lock b/Gemfile.lock index bcd977d68..3d1a3f0d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -343,7 +343,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (3.1.11) + rack (3.1.12) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-mini-profiler (3.3.1) diff --git a/app/controllers/address_search_controller.rb b/app/controllers/address_search_controller.rb index 616d5b702..f1d834a11 100644 --- a/app/controllers/address_search_controller.rb +++ b/app/controllers/address_search_controller.rb @@ -5,7 +5,9 @@ class AddressSearchController < ApplicationController def index query = params[:query] - if query.match?(/\A\d+\z/) && query.length > 5 + if query.nil? + render json: { error: "Query cannot be blank." }, status: :bad_request + elsif query.match?(/\A\d+\z/) && query.length > 5 # Query is all numbers and greater than 5 digits, assume it's a UPRN service = UprnClient.new(query) service.call @@ -16,8 +18,8 @@ class AddressSearchController < ApplicationController presenter = UprnDataPresenter.new(service.result) render json: [{ text: presenter.address, value: presenter.uprn }] end - elsif query.match?(/[a-zA-Z]/) - # Query contains letters, assume it's an address + elsif query.match?(/\D/) + # Query contains any non-digit characters, assume it's an address service = AddressClient.new(query, { minmatch: 0.2 }) service.call @@ -38,7 +40,7 @@ class AddressSearchController < ApplicationController address_service.call uprn_service.call - results = ([uprn_service.result] || []) + (address_service.result || []) + results = [uprn_service.result, *address_service.result].compact if address_service.error.present? && uprn_service.error.present? render json: { error: "Address and UPRN are not recognised." }, status: :not_found diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index 42dadf43c..179d08010 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -18,7 +18,7 @@ module DerivedVariables::LettingsLogVariables 3 => 6, # "Rent to Buy" => "Rent to Buy basis" 4 => 7, # "London Living Rent" => "London Living Rent basis" 5 => 8, # "Other intermediate rent product" => "Another Intermediate Rent basis" - 6 => 9, # "Specified accommodation - exempt accommodation, managed properties, refuges and local authority hostels" => "Specified accommodation - exempt accommodation, manged properties, refuges and local authority hostels" + 6 => 9, # "Specified accommodation - exempt accommodation, managed properties, refuges and local authority hostels" => "Specified accommodation - exempt accommodation, managed properties, refuges and local authority hostels" }.freeze RENTTYPE_DETAIL_MAPPING = { @@ -83,12 +83,19 @@ module DerivedVariables::LettingsLogVariables end set_housingneeds_fields if housingneeds? + if form.start_year_2025_or_later? && is_general_needs? + if changed_to_newbuild? && uprn.nil? + self.manual_address_entry_selected = true + end + + self.manual_address_entry_selected = address_answered_without_uprn? if changed_from_newbuild? + end self.uprn_known = 0 if address_answered_without_uprn? if uprn_known&.zero? self.uprn = nil - if uprn_known_was == 1 + if uprn_known_was == 1 && (rsnvac != 15 || !form.start_year_2025_or_later?) self.address_line1 = nil self.address_line2 = nil self.town_or_city = nil diff --git a/app/models/export.rb b/app/models/export.rb index 27e574c72..d820b2f98 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -1,5 +1,6 @@ class Export < ApplicationRecord scope :lettings, -> { where(collection: "lettings") } + scope :sales, -> { where(collection: "sales") } scope :organisations, -> { where(collection: "organisations") } scope :users, -> { where(collection: "users") } end diff --git a/app/models/form/lettings/questions/previous_let_type.rb b/app/models/form/lettings/questions/previous_let_type.rb index a3b038d6d..8c35df84c 100644 --- a/app/models/form/lettings/questions/previous_let_type.rb +++ b/app/models/form/lettings/questions/previous_let_type.rb @@ -36,7 +36,7 @@ class Form::Lettings::Questions::PreviousLetType < ::Form::Question "6" => { "value" => "Rent to Buy basis" }, "7" => { "value" => "London Living Rent basis" }, "8" => { "value" => "Another Intermediate Rent basis" }, - "9" => { "value" => "Specified accommodation - exempt accommodation, manged properties, refuges and local authority hostels" }, + "9" => { "value" => "Specified accommodation - exempt accommodation, managed properties, refuges and local authority hostels" }, "divider" => { "value" => true }, "3" => { "value" => "Don’t know" }, }.freeze diff --git a/app/models/form/lettings/questions/reason.rb b/app/models/form/lettings/questions/reason.rb index 4cf851f4a..d6254f8fa 100644 --- a/app/models/form/lettings/questions/reason.rb +++ b/app/models/form/lettings/questions/reason.rb @@ -46,9 +46,9 @@ class Form::Lettings::Questions::Reason < ::Form::Question "18" => { "value" => "To move to accommodation with support" }, "19" => { "value" => "To move to independent accommodation" }, "20" => { "value" => "Other" }, - "28" => { "value" => "Don’t know" }, - "divider" => { "value" => true }, "47" => { "value" => "Tenant prefers not to say" }, + "divider" => { "value" => true }, + "28" => { "value" => "Don’t know" }, }.freeze end diff --git a/app/models/form/lettings/subsections/property_information.rb b/app/models/form/lettings/subsections/property_information.rb index 475ff0a8c..3b827bf48 100644 --- a/app/models/form/lettings/subsections/property_information.rb +++ b/app/models/form/lettings/subsections/property_information.rb @@ -56,6 +56,8 @@ class Form::Lettings::Subsections::PropertyInformation < ::Form::Subsection end def displayed_in_tasklist?(log) + return true if form.start_year_2025_or_later? + !(log.is_supported_housing? && log.is_renewal?) end end diff --git a/app/models/form/sales/pages/previous_bedrooms.rb b/app/models/form/sales/pages/previous_bedrooms.rb index 214632d49..41573890d 100644 --- a/app/models/form/sales/pages/previous_bedrooms.rb +++ b/app/models/form/sales/pages/previous_bedrooms.rb @@ -9,6 +9,10 @@ class Form::Sales::Pages::PreviousBedrooms < ::Form::Page { "soctenant" => 0, }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 2 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 2 }, ] end diff --git a/app/models/form/sales/pages/previous_property_type.rb b/app/models/form/sales/pages/previous_property_type.rb index 26669d774..03a8a6384 100644 --- a/app/models/form/sales/pages/previous_property_type.rb +++ b/app/models/form/sales/pages/previous_property_type.rb @@ -11,6 +11,10 @@ class Form::Sales::Pages::PreviousPropertyType < ::Form::Page { "soctenant" => 0, }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 2 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 2 }, ] end diff --git a/app/models/form/sales/pages/previous_tenure.rb b/app/models/form/sales/pages/previous_tenure.rb index 0f4a4b250..7abdb1db5 100644 --- a/app/models/form/sales/pages/previous_tenure.rb +++ b/app/models/form/sales/pages/previous_tenure.rb @@ -12,6 +12,10 @@ class Form::Sales::Pages::PreviousTenure < ::Form::Page { "soctenant" => 0, }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 2 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 2 }, ] end diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 920aed628..4554b99d2 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -148,7 +148,7 @@ class LettingsLog < Log OPTIONAL_FIELDS = %w[tenancycode propcode chcharge].freeze RENT_TYPE_MAPPING_LABELS = { 1 => "Social Rent", 2 => "Affordable Rent", 3 => "Intermediate Rent", 4 => "Specified accommodation" }.freeze HAS_BENEFITS_OPTIONS = [1, 6, 8, 7].freeze - NUM_OF_WEEKS_FROM_PERIOD = { 2 => 26, 3 => 13, 4 => 12, 5 => 50, 6 => 49, 7 => 48, 8 => 47, 9 => 46, 1 => 52, 10 => 53 }.freeze + NUM_OF_WEEKS_FROM_PERIOD = { 2 => 26, 3 => 13, 4 => 12, 5 => 50, 6 => 49, 7 => 48, 8 => 47, 9 => 46, 11 => 51, 1 => 52, 10 => 53 }.freeze SUFFIX_FROM_PERIOD = { 2 => "every 2 weeks", 3 => "every 4 weeks", 4 => "every month" }.freeze DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id tenancycode startdate age1_known age1 sex1 ecstat1 tcharge household_charge chcharge].freeze RENT_TYPE = { @@ -626,7 +626,7 @@ class LettingsLog < Log end def rent_and_charges_paid_weekly? - [1, 5, 6, 7, 8, 9, 10].include? period + [1, 5, 6, 7, 8, 9, 10, 11].include? period end def rent_and_charges_paid_every_4_weeks? @@ -740,6 +740,14 @@ class LettingsLog < Log "lettings_log" end + def changed_to_newbuild? + rsnvac == 15 && rsnvac_was != 15 + end + + def changed_from_newbuild? + rsnvac != 15 && rsnvac_was == 15 + end + private def reset_invalid_unresolved_log_fields! diff --git a/app/models/validations/sales/financial_validations.rb b/app/models/validations/sales/financial_validations.rb index e5d8232f0..ae98184c9 100644 --- a/app/models/validations/sales/financial_validations.rb +++ b/app/models/validations/sales/financial_validations.rb @@ -75,7 +75,7 @@ module Validations::Sales::FinancialValidations if threshold && record.stairbought < threshold shared_ownership_type = record.form.get_question("type", record).label_from_value(record.type).downcase record.errors.add :stairbought, I18n.t("validations.sales.financial.stairbought.percentage_bought_must_be_at_least_threshold", threshold:, shared_ownership_type:) - record.errors.add :type, I18n.t("validations.sales.financial.type.percentage_bought_must_be_at_least_threshold", threshold:, shared_ownership_type:) + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.financial.type.percentage_bought_must_be_at_least_threshold", threshold:, shared_ownership_type:) end end @@ -96,10 +96,10 @@ module Validations::Sales::FinancialValidations return unless (range = ranges[record.type]) if record.equity < range.min - record.errors.add :type, I18n.t("validations.sales.financial.type.equity_under_min", min_equity: range.min) + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.financial.type.equity_under_min", min_equity: range.min) record.errors.add :equity, :under_min, message: I18n.t("validations.sales.financial.equity.equity_under_min", min_equity: range.min) elsif !record.is_resale? && record.equity > range.max - record.errors.add :type, I18n.t("validations.sales.financial.type.equity_over_max", max_equity: range.max) + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.financial.type.equity_over_max", max_equity: range.max) record.errors.add :equity, :over_max, message: I18n.t("validations.sales.financial.equity.equity_over_max", max_equity: range.max) record.errors.add :resale, I18n.t("validations.sales.financial.resale.equity_over_max", max_equity: range.max) end diff --git a/app/models/validations/sales/property_validations.rb b/app/models/validations/sales/property_validations.rb index 7fd4d2440..56a09c36f 100644 --- a/app/models/validations/sales/property_validations.rb +++ b/app/models/validations/sales/property_validations.rb @@ -49,9 +49,9 @@ module Validations::Sales::PropertyValidations record.errors.add :uprn_confirmation, I18n.t("validations.sales.property_information.uprn_confirmation.not_in_england") record.errors.add :uprn_selection, I18n.t("validations.sales.property_information.uprn_selection.not_in_england") if record.uprn.present? - record.errors.add :saledate, I18n.t("validations.sales.property_information.saledate.address_not_in_england") + record.errors.add :saledate, :skip_bu_error, message: I18n.t("validations.sales.property_information.saledate.address_not_in_england") else - record.errors.add :saledate, I18n.t("validations.sales.property_information.saledate.postcode_not_in_england") + record.errors.add :saledate, :skip_bu_error, message: I18n.t("validations.sales.property_information.saledate.postcode_not_in_england") end end end diff --git a/app/models/validations/sales/sale_information_validations.rb b/app/models/validations/sales/sale_information_validations.rb index f99f6668a..3ddecaedb 100644 --- a/app/models/validations/sales/sale_information_validations.rb +++ b/app/models/validations/sales/sale_information_validations.rb @@ -108,7 +108,7 @@ module Validations::Sales::SaleInformationValidations if record.shared_ownership_scheme? && !record.old_persons_shared_ownership? && record.mrent > 9999 record.errors.add :mrent, I18n.t("validations.sales.sale_information.mrent.monthly_rent_higher_than_expected") - record.errors.add :type, I18n.t("validations.sales.sale_information.type.monthly_rent_higher_than_expected") + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.sale_information.type.monthly_rent_higher_than_expected") end end @@ -136,7 +136,7 @@ module Validations::Sales::SaleInformationValidations if max_stairbought && record.stairbought > max_stairbought record.errors.add :stairbought, I18n.t("validations.sales.sale_information.stairbought.stairbought_over_max", max_stairbought:, type: record.form.get_question("type", record).answer_label(record)) - record.errors.add :type, I18n.t("validations.sales.sale_information.type.stairbought_over_max", max_stairbought:, type: record.form.get_question("type", record).answer_label(record)) + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.sale_information.type.stairbought_over_max", max_stairbought:, type: record.form.get_question("type", record).answer_label(record)) end end diff --git a/app/models/validations/soft_validations.rb b/app/models/validations/soft_validations.rb index 9dc8828e8..2f2a9eb89 100644 --- a/app/models/validations/soft_validations.rb +++ b/app/models/validations/soft_validations.rb @@ -103,13 +103,16 @@ module Validations::SoftValidations TWO_YEARS_IN_DAYS = 730 TEN_YEARS_IN_DAYS = 3650 + TWENTY_YEARS_IN_DAYS = 7300 def major_repairs_date_in_soft_range? - mrcdate.present? && startdate.present? && mrcdate.between?(startdate.to_date - TEN_YEARS_IN_DAYS, startdate.to_date - TWO_YEARS_IN_DAYS) + upper_limit = form.start_year_2025_or_later? ? TWENTY_YEARS_IN_DAYS : TEN_YEARS_IN_DAYS + mrcdate.present? && startdate.present? && mrcdate.between?(startdate.to_date - upper_limit, startdate.to_date - TWO_YEARS_IN_DAYS) end def voiddate_in_soft_range? - voiddate.present? && startdate.present? && voiddate.between?(startdate.to_date - TEN_YEARS_IN_DAYS, startdate.to_date - TWO_YEARS_IN_DAYS) + upper_limit = form.start_year_2025_or_later? ? TWENTY_YEARS_IN_DAYS : TEN_YEARS_IN_DAYS + voiddate.present? && startdate.present? && voiddate.between?(startdate.to_date - upper_limit, startdate.to_date - TWO_YEARS_IN_DAYS) end def net_income_higher_or_lower_text diff --git a/app/services/bulk_upload/sales/year2024/row_parser.rb b/app/services/bulk_upload/sales/year2024/row_parser.rb index 5f066697c..a613a118c 100644 --- a/app/services/bulk_upload/sales/year2024/row_parser.rb +++ b/app/services/bulk_upload/sales/year2024/row_parser.rb @@ -252,8 +252,8 @@ class BulkUpload::Sales::Year2024::RowParser attribute :field_84, :integer attribute :field_85, :integer attribute :field_86, :integer - attribute :field_87, :integer - attribute :field_88, :integer + attribute :field_87, :decimal + attribute :field_88, :decimal attribute :field_89, :integer attribute :field_90, :integer @@ -268,40 +268,40 @@ class BulkUpload::Sales::Year2024::RowParser attribute :field_99, :integer attribute :field_100, :integer - attribute :field_101, :integer - attribute :field_102, :integer + attribute :field_101, :decimal + attribute :field_102, :decimal attribute :field_103, :integer - attribute :field_104, :integer + attribute :field_104, :decimal attribute :field_105, :integer attribute :field_106, :string attribute :field_107, :integer attribute :field_108, :integer - attribute :field_109, :integer - attribute :field_110, :integer + attribute :field_109, :decimal + attribute :field_110, :decimal attribute :field_111, :decimal attribute :field_112, :decimal attribute :field_113, :integer - attribute :field_114, :integer + attribute :field_114, :decimal attribute :field_115, :integer - attribute :field_116, :integer + attribute :field_116, :decimal attribute :field_117, :integer - attribute :field_118, :integer + attribute :field_118, :decimal attribute :field_119, :integer attribute :field_120, :string attribute :field_121, :integer attribute :field_122, :integer - attribute :field_123, :integer - attribute :field_124, :integer - attribute :field_125, :integer + attribute :field_123, :decimal + attribute :field_124, :decimal + attribute :field_125, :decimal attribute :field_126, :integer - attribute :field_127, :integer + attribute :field_127, :decimal attribute :field_128, :integer attribute :field_129, :integer - attribute :field_130, :integer - attribute :field_131, :integer + attribute :field_130, :decimal + attribute :field_131, :decimal validates :field_4, presence: { diff --git a/app/services/bulk_upload/sales/year2025/row_parser.rb b/app/services/bulk_upload/sales/year2025/row_parser.rb index feb607baf..886b3e928 100644 --- a/app/services/bulk_upload/sales/year2025/row_parser.rb +++ b/app/services/bulk_upload/sales/year2025/row_parser.rb @@ -236,19 +236,19 @@ class BulkUpload::Sales::Year2025::RowParser attribute :field_83, :integer attribute :field_84, :integer attribute :field_85, :integer - attribute :field_86, :integer - attribute :field_87, :integer + attribute :field_86, :decimal + attribute :field_87, :decimal attribute :field_88, :integer - attribute :field_89, :integer + attribute :field_89, :decimal attribute :field_90, :integer - attribute :field_91, :integer - attribute :field_92, :integer + attribute :field_91, :decimal + attribute :field_92, :decimal attribute :field_93, :decimal attribute :field_94, :decimal attribute :field_95, :decimal - attribute :field_96, :integer - attribute :field_97, :integer + attribute :field_96, :decimal + attribute :field_97, :decimal attribute :field_98, :integer attribute :field_99, :integer attribute :field_100, :integer @@ -258,22 +258,22 @@ class BulkUpload::Sales::Year2025::RowParser attribute :field_104, :integer attribute :field_105, :integer attribute :field_106, :integer - attribute :field_107, :integer - attribute :field_108, :integer + attribute :field_107, :decimal + attribute :field_108, :decimal attribute :field_109, :integer - attribute :field_110, :integer + attribute :field_110, :decimal attribute :field_111, :decimal attribute :field_112, :integer - attribute :field_113, :integer + attribute :field_113, :decimal attribute :field_114, :integer - attribute :field_115, :integer + attribute :field_115, :decimal attribute :field_116, :integer - attribute :field_117, :integer + attribute :field_117, :decimal attribute :field_118, :integer attribute :field_119, :integer - attribute :field_120, :integer - attribute :field_121, :integer + attribute :field_120, :decimal + attribute :field_121, :decimal validates :field_1, presence: { @@ -393,6 +393,15 @@ class BulkUpload::Sales::Year2025::RowParser }, on: :after_log + validates :field_103, + numericality: { + greater_than_or_equal_to: 2, + less_than_or_equal_to: 10, + message: I18n.t("#{ERROR_BASE_KEY}.numeric.within_range", field: "Number of staircasing transactions", min: "2", max: "10"), + allow_blank: true, + }, + on: :before_log + validate :validate_buyer1_economic_status, on: :before_log validate :validate_buyer2_economic_status, on: :before_log validate :validate_valid_radio_option, on: :before_log @@ -806,31 +815,11 @@ private attributes["sex5"] = field_52 attributes["sex6"] = field_56 - attributes["relat2"] = if field_34 == 1 - "P" - else - (field_34 == 2 ? "X" : "R") - end - attributes["relat3"] = if field_42 == 1 - "P" - else - (field_42 == 2 ? "X" : "R") - end - attributes["relat4"] = if field_46 == 1 - "P" - else - (field_46 == 2 ? "X" : "R") - end - attributes["relat5"] = if field_50 == 1 - "P" - else - (field_50 == 2 ? "X" : "R") - end - attributes["relat6"] = if field_54 == 1 - "P" - else - (field_54 == 2 ? "X" : "R") - end + attributes["relat2"] = relationship_from_is_partner(field_34) + attributes["relat3"] = relationship_from_is_partner(field_42) + attributes["relat4"] = relationship_from_is_partner(field_46) + attributes["relat5"] = relationship_from_is_partner(field_50) + attributes["relat6"] = relationship_from_is_partner(field_54) attributes["ecstat1"] = field_32 attributes["ecstat2"] = field_39 @@ -1043,6 +1032,17 @@ private field_55.present? || field_56.present? || field_54.present? end + def relationship_from_is_partner(is_partner) + case is_partner + when 1 + "P" + when 2 + "X" + when 3 + "R" + end + end + def details_known?(person_n) send("person_#{person_n}_present?") ? 1 : 2 end @@ -1482,6 +1482,17 @@ private %w[0] + GlobalConstants::COUNTRIES_ANSWER_OPTIONS.keys # 0 is "Prefers not to say" end + def validate_relat_fields + %i[field_34 field_42 field_46 field_50 field_54].each do |field| + value = send(field) + next if value.blank? + + unless (1..3).cover?(value) + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field]))) + end + end + end + def bulk_upload_organisation Organisation.find(bulk_upload.organisation_id) end diff --git a/app/services/csv/lettings_log_csv_service.rb b/app/services/csv/lettings_log_csv_service.rb index 0a187d524..2da392fb0 100644 --- a/app/services/csv/lettings_log_csv_service.rb +++ b/app/services/csv/lettings_log_csv_service.rb @@ -177,6 +177,10 @@ module Csv 10 => "Intermediate rent supported housing private registered provider", 11 => "Intermediate rent general needs local authority", 12 => "Intermediate rent supported housing local authority", + 13 => "Specified accommodation general needs private registered provider", + 14 => "Specified accommodation supported housing private registered provider", + 15 => "Specified accommodation general needs local authority", + 16 => "Specified accommodation supported housing local authority", }.freeze IRPRODUCT_LABELS = { @@ -206,6 +210,7 @@ module Csv 1 => "Social Rent", 2 => "Affordable Rent", 3 => "Intermediate Rent", + 4 => "Specified accommodation", }.freeze UPRN_KNOWN_LABELS = { @@ -284,13 +289,12 @@ module Csv end def lettings_log_definitions - CsvVariableDefinition.lettings.group_by { |record| [record.variable, record.definition] } - .map do |_, options| + CsvVariableDefinition.lettings.group_by(&:variable).map { |_, options| exact_match = options.find { |definition| definition.year == @year } next exact_match if exact_match - options.max_by(&:year) - end + options.select { |opt| opt.year < @year }.max_by(&:year) + }.compact end def insert_derived_and_related_attributes(ordered_questions) diff --git a/app/services/csv/sales_log_csv_service.rb b/app/services/csv/sales_log_csv_service.rb index 08ce178e3..35adcf27f 100644 --- a/app/services/csv/sales_log_csv_service.rb +++ b/app/services/csv/sales_log_csv_service.rb @@ -179,13 +179,12 @@ module Csv end def sales_log_definitions - CsvVariableDefinition.sales.group_by { |record| [record.variable, record.definition] } - .map do |_, options| + CsvVariableDefinition.sales.group_by(&:variable).map { |_, options| exact_match = options.find { |definition| definition.year == @year } next exact_match if exact_match - options.max_by(&:year) - end + options.select { |opt| opt.year < @year }.max_by(&:year) + }.compact end def insert_derived_and_related_attributes(ordered_questions) diff --git a/app/services/exports/export_service.rb b/app/services/exports/export_service.rb index 40c10055b..d828fdc49 100644 --- a/app/services/exports/export_service.rb +++ b/app/services/exports/export_service.rb @@ -11,6 +11,7 @@ module Exports start_time = Time.zone.now daily_run_number = get_daily_run_number lettings_archives_for_manifest = {} + sales_archives_for_manifest = {} users_archives_for_manifest = {} organisations_archives_for_manifest = {} @@ -20,16 +21,19 @@ module Exports users_archives_for_manifest = get_user_archives(start_time, full_update) when "organisations" organisations_archives_for_manifest = get_organisation_archives(start_time, full_update) - else + when "lettings" lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, year) + when "sales" + sales_archives_for_manifest = get_sales_archives(start_time, full_update, year) end else users_archives_for_manifest = get_user_archives(start_time, full_update) organisations_archives_for_manifest = get_organisation_archives(start_time, full_update) lettings_archives_for_manifest = get_lettings_archives(start_time, full_update, year) + sales_archives_for_manifest = get_sales_archives(start_time, full_update, year) if FeatureToggle.sales_export_enabled? end - write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(users_archives_for_manifest).merge(organisations_archives_for_manifest)) + write_master_manifest(daily_run_number, lettings_archives_for_manifest.merge(sales_archives_for_manifest).merge(users_archives_for_manifest).merge(organisations_archives_for_manifest)) end private @@ -74,5 +78,10 @@ module Exports lettings_export_service = Exports::LettingsLogExportService.new(@storage_service, start_time) lettings_export_service.export_xml_lettings_logs(full_update:, collection_year:) end + + def get_sales_archives(start_time, full_update, collection_year) + sales_export_service = Exports::SalesLogExportService.new(@storage_service, start_time) + sales_export_service.export_xml_sales_logs(full_update:, collection_year:) + end end end diff --git a/app/services/exports/lettings_log_export_constants.rb b/app/services/exports/lettings_log_export_constants.rb index 3dc47736a..b0298e6ca 100644 --- a/app/services/exports/lettings_log_export_constants.rb +++ b/app/services/exports/lettings_log_export_constants.rb @@ -193,5 +193,10 @@ module Exports::LettingsLogExportConstants PRE_2024_EXPORT_FIELDS = Set[ "national", "offered" +] + + PRE_2025_EXPORT_FIELDS = Set[ + "carehome_charges_value_check", + "chcharge" ] end diff --git a/app/services/exports/lettings_log_export_service.rb b/app/services/exports/lettings_log_export_service.rb index cf3e984ae..c52bf8635 100644 --- a/app/services/exports/lettings_log_export_service.rb +++ b/app/services/exports/lettings_log_export_service.rb @@ -142,7 +142,8 @@ module Exports pattern_age.match(field_name) || !EXPORT_FIELDS.include?(field_name) || (lettings_log.form.start_year_2024_or_later? && PRE_2024_EXPORT_FIELDS.include?(field_name)) || - (!lettings_log.form.start_year_2024_or_later? && POST_2024_EXPORT_FIELDS.include?(field_name)) + (!lettings_log.form.start_year_2024_or_later? && POST_2024_EXPORT_FIELDS.include?(field_name)) || + (lettings_log.form.start_year_2025_or_later? && PRE_2025_EXPORT_FIELDS.include?(field_name)) end def build_export_xml(lettings_logs) diff --git a/app/services/exports/sales_log_export_constants.rb b/app/services/exports/sales_log_export_constants.rb new file mode 100644 index 000000000..09a0d5d68 --- /dev/null +++ b/app/services/exports/sales_log_export_constants.rb @@ -0,0 +1,146 @@ +module Exports::SalesLogExportConstants + MAX_XML_RECORDS = 10_000 + LOG_ID_OFFSET = 300_000_000_000 + + EXPORT_MODE = { + xml: 1, + csv: 2, + }.freeze + + EXPORT_FIELDS = Set["ID", + "STATUS", + "DAY", + "MONTH", + "YEAR", + "DUPLICATESET", + "CREATEDDATE", + "UPLOADDATE", + "OWNINGORGID", + "OWNINGORGNAME", + "MANINGORGID", + "MANINGORGNAME", + "USERNAME", + "USERNAMEID", + "PURCHID", + "TYPE", + "OWNERSHIP", + "COLLECTIONYEAR", + "JOINTMORE", + "JOINT", + "BEDS", + "ETHNIC", + "ETHNICGROUP1", + "LIVEINBUYER1", + "BUILTYPE", + "PROPTYPE", + "NOINT", + "LIVEINBUYER2", + "PRIVACYNOTICE", + "WHEEL", + "HHOLDCOUNT", + "LA", + "INCOME1", + "INC1NK", + "INC1MORT", + "INCOME2", + "INC2NK", + "SAVINGSNK", + "SAVINGS", + "PREVOWN", + "AMENDEDBY", + "AMENDEDBYID", + "MORTGAGE", + "INC2MORT", + "HB", + "FROMBEDS", + "STAIRCASE", + "STAIRBOUGHT", + "STAIROWNED", + "MRENT", + "MRENTPRESTAIRCASING", + "RESALE", + "DEPOSIT", + "CASHDIS", + "DISABLED", + "VALUE", + "EQUITY", + "DISCOUNT", + "GRANT", + "PPCODENK", + "PPOSTC1", + "PPOSTC2", + "PREVLOC", + "PREVLOCNAME", + "PREVIOUSLAKNOWN", + "HHREGRES", + "HHREGRESSTILL", + "PROPLEN", + "HASMSCHARGE", + "MSCHARGE", + "PREVTEN", + "MORTGAGEUSED", + "WCHAIR", + "ARMEDFORCESSPOUSE", + "HODAY", + "HOMONTH", + "HOYEAR", + "FROMPROP", + "SOCPREVTEN", + "MORTLEN1", + "EXTRABOR", + "HHTYPE", + "POSTCODE", + "ISLAINFERRED", + "BULKUPLOADID", + "VALUE_VALUE_CHECK", + "PREVSHARED", + "STAIRCASETOSALE", + "ETHNICGROUP2", + "ETHNIC2", + "BUY2LIVING", + "PREVTEN2", + "UPRN", + "ADDRESS1", + "ADDRESS2", + "TOWNCITY", + "COUNTY", + "LANAME", + "CREATIONMETHOD", + "NATIONALITYALL1", + "NATIONALITYALL2", + "MSCHARGE_VALUE_CHECK", + "ADDRESS1INPUT", + "POSTCODEINPUT", + "ADDRESS_SEARCH_VALUE_CHECK", + "UPRNSELECTED", + "BULKADDRESS1", + "BULKADDRESS2", + "BULKTOWNCITY", + "BULKCOUNTY", + "BULKPOSTCODE", + "BULKLA", + "CREATEDBY", + "CREATEDBYID", + "HASESTATEFEE", + "ESTATEFEE", + "FIRSTSTAIR", + "NUMSTAIR", + "STAIRLASTDAY", + "STAIRLASTMONTH", + "STAIRLASTYEAR", + "STAIRINITIALYEAR", + "STAIRINITIALMONTH", + "STAIRINITIALDAY", + "HASSERVICECHARGES", + "SERVICECHARGES",] + + (1..6).each do |index| + EXPORT_FIELDS << "AGE#{index}" + EXPORT_FIELDS << "ECSTAT#{index}" + EXPORT_FIELDS << "SEX#{index}" + end + + (2..6).each do |index| + EXPORT_FIELDS << "RELAT#{index}" + end +end diff --git a/app/services/exports/sales_log_export_service.rb b/app/services/exports/sales_log_export_service.rb new file mode 100644 index 000000000..fa806f6ab --- /dev/null +++ b/app/services/exports/sales_log_export_service.rb @@ -0,0 +1,156 @@ +module Exports + class SalesLogExportService < Exports::XmlExportService + include Exports::SalesLogExportConstants + include CollectionTimeHelper + + def export_xml_sales_logs(full_update: false, collection_year: nil) + archives_for_manifest = {} + collection_years_to_export(collection_year).each do |year| + recent_export = Export.sales.where(year:).order("started_at").last + base_number = Export.sales.where(empty_export: false, year:).maximum(:base_number) || 1 + export = build_export_run("sales", base_number, full_update, year) + archives = write_export_archive(export, year, recent_export, full_update) + + archives_for_manifest.merge!(archives) + + export.empty_export = archives.empty? + export.save! + end + + archives_for_manifest + end + + private + + def get_archive_name(year, base_number, increment) + return unless year + + base_number_str = "f#{base_number.to_s.rjust(4, '0')}" + increment_str = "inc#{increment.to_s.rjust(4, '0')}" + "core_sales_#{year}_#{year + 1}_apr_mar_#{base_number_str}_#{increment_str}".downcase + end + + def retrieve_resources(recent_export, full_update, year) + if !full_update && recent_export + params = { from: recent_export.started_at, to: @start_time } + SalesLog.exportable.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params).filter_by_year(year) + else + params = { to: @start_time } + SalesLog.exportable.where("updated_at <= :to", params).filter_by_year(year) + end + end + + def apply_cds_transformation(sales_log, _export_mode) + attribute_hash = sales_log.attributes_before_type_cast + + attribute_hash["day"] = sales_log.saledate&.day + attribute_hash["month"] = sales_log.saledate&.month + attribute_hash["year"] = sales_log.saledate&.year + + attribute_hash["createddate"] = sales_log.created_at&.iso8601 + attribute_hash["createdby"] = sales_log.created_by&.email + attribute_hash["createdbyid"] = sales_log.created_by_id + attribute_hash["username"] = sales_log.assigned_to&.email + attribute_hash["usernameid"] = sales_log.assigned_to_id + attribute_hash["uploaddate"] = sales_log.updated_at&.iso8601 + attribute_hash["amendedby"] = sales_log.updated_by&.email + attribute_hash["amendedbyid"] = sales_log.updated_by_id + + attribute_hash["owningorgid"] = sales_log.owning_organisation&.id + attribute_hash["owningorgname"] = sales_log.owning_organisation&.name + attribute_hash["maningorgid"] = sales_log.managing_organisation&.id + attribute_hash["maningorgname"] = sales_log.managing_organisation&.name + + attribute_hash["creationmethod"] = sales_log.creation_method_before_type_cast + attribute_hash["bulkuploadid"] = sales_log.bulk_upload_id + attribute_hash["collectionyear"] = sales_log.form.start_date.year + attribute_hash["ownership"] = sales_log.ownershipsch + attribute_hash["joint"] = sales_log.jointpur + attribute_hash["ethnicgroup1"] = sales_log.ethnic_group + attribute_hash["ethnicgroup2"] = sales_log.ethnic_group2 + attribute_hash["previouslaknown"] = sales_log.previous_la_known + attribute_hash["hasmscharge"] = sales_log.discounted_ownership_sale? ? sales_log.has_mscharge : nil + attribute_hash["mscharge"] = sales_log.discounted_ownership_sale? ? sales_log.mscharge : nil + attribute_hash["hasservicecharges"] = sales_log.shared_ownership_scheme? ? sales_log.has_mscharge : nil + attribute_hash["servicecharges"] = sales_log.shared_ownership_scheme? ? sales_log.mscharge : nil + + attribute_hash["hoday"] = sales_log.hodate&.day + attribute_hash["homonth"] = sales_log.hodate&.month + attribute_hash["hoyear"] = sales_log.hodate&.year + + attribute_hash["inc1nk"] = sales_log.income1nk + attribute_hash["inc2nk"] = sales_log.income2nk + attribute_hash["postcode"] = sales_log.postcode_full + attribute_hash["islainferred"] = sales_log.is_la_inferred + attribute_hash["mortlen1"] = sales_log.mortlen + attribute_hash["ethnic2"] = sales_log.ethnicbuy2 + attribute_hash["prevten2"] = sales_log.prevtenbuy2 + + attribute_hash["address1"] = sales_log.address_line1 + attribute_hash["address2"] = sales_log.address_line2 + attribute_hash["towncity"] = sales_log.town_or_city + attribute_hash["laname"] = LocalAuthority.find_by(code: sales_log.la)&.name + attribute_hash["address1input"] = sales_log.address_line1_input + attribute_hash["postcodeinput"] = sales_log.postcode_full_input + attribute_hash["uprnselected"] = sales_log.uprn_selection + + attribute_hash["bulkaddress1"] = sales_log.address_line1_as_entered + attribute_hash["bulkaddress2"] = sales_log.address_line2_as_entered + attribute_hash["bulktowncity"] = sales_log.town_or_city_as_entered + attribute_hash["bulkcounty"] = sales_log.county_as_entered + attribute_hash["bulkpostcode"] = sales_log.postcode_full_as_entered + attribute_hash["bulkla"] = sales_log.la_as_entered + attribute_hash["nationalityall1"] = sales_log.nationality_all + attribute_hash["nationalityall2"] = sales_log.nationality_all_buyer2 + attribute_hash["prevlocname"] = LocalAuthority.find_by(code: sales_log.prevloc)&.name + attribute_hash["liveinbuyer1"] = sales_log.buy1livein + attribute_hash["liveinbuyer2"] = sales_log.buy2livein + + attribute_hash["hasestatefee"] = sales_log.has_management_fee + attribute_hash["estatefee"] = sales_log.management_fee + + attribute_hash["stairlastday"] = sales_log.lasttransaction&.day + attribute_hash["stairlastmonth"] = sales_log.lasttransaction&.month + attribute_hash["stairlastyear"] = sales_log.lasttransaction&.year + + attribute_hash["stairinitialday"] = sales_log.initialpurchase&.day + attribute_hash["stairinitialmonth"] = sales_log.initialpurchase&.month + attribute_hash["stairinitialyear"] = sales_log.initialpurchase&.year + attribute_hash["mscharge_value_check"] = sales_log.monthly_charges_value_check + attribute_hash["duplicateset"] = sales_log.duplicate_set_id + attribute_hash["staircasetosale"] = sales_log.staircasesale + + attribute_hash.transform_keys!(&:upcase) + attribute_hash + end + + def is_omitted_field?(field_name, _sales_log) + !EXPORT_FIELDS.include?(field_name) + end + + def build_export_xml(sales_logs) + doc = Nokogiri::XML("") + + sales_logs.each do |sales_log| + attribute_hash = apply_cds_transformation(sales_log, EXPORT_MODE[:xml]) + form = doc.create_element("form") + doc.at("forms") << form + attribute_hash.each do |key, value| + if is_omitted_field?(key, sales_log) + next + else + form << doc.create_element(key, value) + end + end + end + + xml_doc_to_temp_file(doc) + end + + def collection_years_to_export(collection_year) + return [collection_year] if collection_year.present? + + FormHandler.instance.sales_forms.values.map { |f| f.start_date.year }.uniq.select { |year| year > 2024 } + end + end +end diff --git a/app/services/exports/user_export_service.rb b/app/services/exports/user_export_service.rb index 707d13c14..177daee1d 100644 --- a/app/services/exports/user_export_service.rb +++ b/app/services/exports/user_export_service.rb @@ -28,7 +28,7 @@ module Exports def retrieve_resources(recent_export, full_update, _year) if !full_update && recent_export params = { from: recent_export.started_at, to: @start_time } - User.where("(updated_at >= :from AND updated_at <= :to)", params) + User.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params) else params = { to: @start_time } User.where("updated_at <= :to", params) diff --git a/app/services/feature_toggle.rb b/app/services/feature_toggle.rb index 93a92dcd4..4b2c440fc 100644 --- a/app/services/feature_toggle.rb +++ b/app/services/feature_toggle.rb @@ -30,4 +30,8 @@ class FeatureToggle def self.create_test_logs_enabled? Rails.env.development? || Rails.env.review? end + + def self.sales_export_enabled? + Time.zone.now >= Time.zone.local(2025, 4, 1) || (Rails.env.review? || Rails.env.staging?) + end end diff --git a/app/services/merge/merge_organisations_service.rb b/app/services/merge/merge_organisations_service.rb index bcaad85a6..40749b54c 100644 --- a/app/services/merge/merge_organisations_service.rb +++ b/app/services/merge/merge_organisations_service.rb @@ -62,7 +62,7 @@ private def merge_users(merging_organisation) users_to_merge = users_to_merge(merging_organisation) @merged_users[merging_organisation.name] = users_to_merge.map { |user| { name: user.name, email: user.email } } - users_to_merge.update_all(organisation_id: @absorbing_organisation.id) + users_to_merge.update_all(organisation_id: @absorbing_organisation.id, values_updated_at: Time.zone.now) end def merge_schemes_and_locations(merging_organisation) diff --git a/app/services/uprn_client.rb b/app/services/uprn_client.rb index 97dc4753f..f847c7da5 100644 --- a/app/services/uprn_client.rb +++ b/app/services/uprn_client.rb @@ -20,7 +20,16 @@ class UprnClient end def result - @result ||= JSON.parse(response.body).dig("results", 0, "DPA") || JSON.parse(response.body).dig("results", 0, "LPI") + @result ||= if response.is_a?(Net::HTTPSuccess) + parsed_response = JSON.parse(response.body) + parsed_response.dig("results", 0, "DPA") || parsed_response.dig("results", 0, "LPI") + else + @error = "UPRN client failed to return a valid result, try again later." + Sentry.capture_message("UPRN client failed to return a valid result with error code: #{response.code}.") + Rails.logger.error("UPRN client failed to return a valid result with error code: #{response.code}.") + Rails.logger.error("Response body: #{response.body}") + nil + end end private diff --git a/app/views/form/_address_search_question.html.erb b/app/views/form/_address_search_question.html.erb index ea30be718..76b39709a 100644 --- a/app/views/form/_address_search_question.html.erb +++ b/app/views/form/_address_search_question.html.erb @@ -17,7 +17,7 @@ <%= question.answer_selected?(@log, answer) ? "selected" : "" %>><%= answer.name || answer.resource %> <% end %> <% else %> - + <% end %> <% end %> diff --git a/app/views/form/_date_question.html.erb b/app/views/form/_date_question.html.erb index 55193b01a..a1df5f4d5 100644 --- a/app/views/form/_date_question.html.erb +++ b/app/views/form/_date_question.html.erb @@ -1,9 +1,10 @@ <%= render partial: "form/guidance/#{question.top_guidance_partial}" if question.top_guidance? %> +<% legend = legend(question, page_header, conditional) %> <%= render partial: "components/date_picker", locals: { resource: @log, question_id: question.id, - legend: { text: legend(question, page_header, conditional)[:text], size: "l", caption: caption(caption_text, page_header, conditional) }, + legend: { text: legend[:text], size: legend[:size], caption: caption(caption_text, page_header, conditional) }, resource_type: @log.log_type, hint: (question.hint_text.blank? ? "" : (question.hint_text.html_safe + "

".html_safe)) + "For example, #{date_mid_collection_year_formatted(@log.startdate).tr(' ', '/')}", f:, diff --git a/config/routes.rb b/config/routes.rb index 304d54ef0..6b1b6458b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,7 +40,6 @@ Rails.application.routes.draw do get "/service-moved", to: "maintenance#service_moved" get "/service-unavailable", to: "maintenance#service_unavailable" get "/address-search", to: "address_search#index" - get "/address-search/current", to: "address_search#current" get "/address-search/manual-input/:log_type/:log_id", to: "address_search#manual_input", as: "address_manual_input" get "/address-search/search-input/:log_type/:log_id", to: "address_search#search_input", as: "address_search_input" diff --git a/db/migrate/20250305092900_add_values_updated_at_to_user.rb b/db/migrate/20250305092900_add_values_updated_at_to_user.rb new file mode 100644 index 000000000..8ebe67af4 --- /dev/null +++ b/db/migrate/20250305092900_add_values_updated_at_to_user.rb @@ -0,0 +1,5 @@ +class AddValuesUpdatedAtToUser < ActiveRecord::Migration[7.2] + def change + add_column :users, :values_updated_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 82d67b432..e29560461 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_02_25_180643) do +ActiveRecord::Schema[7.2].define(version: 2025_03_05_092900) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -853,6 +853,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_25_180643) do t.boolean "reactivate_with_organisation" t.datetime "discarded_at" t.string "phone_extension" + t.datetime "values_updated_at" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true diff --git a/docs/images/architecture_diagram.png b/docs/images/architecture_diagram.png new file mode 100644 index 000000000..66a02289b Binary files /dev/null and b/docs/images/architecture_diagram.png differ diff --git a/docs/images/context_diagram.png b/docs/images/context_diagram.png new file mode 100644 index 000000000..d829920e0 Binary files /dev/null and b/docs/images/context_diagram.png differ diff --git a/docs/infrastructure.md b/docs/infrastructure.md index fa6b8f2c6..1d095b03f 100644 --- a/docs/infrastructure.md +++ b/docs/infrastructure.md @@ -118,3 +118,6 @@ Things to watch out for when destroying/creating infra: - After destroying the db, you’ll need to make sure the ad hoc ECS task which seeds the database gets run in order to set up the database correctly. - SNS - When creating an email subscription in an environment, Terraform will look up the email to use as the subscription endpoint from Secrets Manager. If you haven’t already created this (e.g. by running terraform apply -target="module.monitoring" -var="create_secrets_first=true") then this will lead to the subscription creation erroring, because it can’t retrieve the value of the secret (because it doesn’t exist yet). If this happens, remember you’ll need to go to Secrets Manager in the console and enter the desired email (as plaintext, no quotation marks or anything else required) as the value of the secret (which is most likely called MONITORING_EMAIL). Then run another apply with Terraform and this time it should succeed. + +![Architecture Diagram](https://raw.githubusercontent.com/communitiesuk/submit-social-housing-lettings-and-sales-data/main/docs/images/architecture_diagram.png) +![Context Diagram](https://raw.githubusercontent.com/communitiesuk/submit-social-housing-lettings-and-sales-data/main/docs/images/context_diagram.png) diff --git a/lib/tasks/update_manual_address_entry_selected_prexisting_logs.rake b/lib/tasks/update_manual_address_entry_selected_prexisting_logs.rake new file mode 100644 index 000000000..d11dc3219 --- /dev/null +++ b/lib/tasks/update_manual_address_entry_selected_prexisting_logs.rake @@ -0,0 +1,207 @@ +namespace :bulk_update do + desc "Update logs with specific criteria and set manual_address_entry_selected to true" + task update_manual_address_entry_selected: :environment do + updated_lettings_logs_count = 0 + lettings_postcode_fixed_count = 0 + lettings_postcode_fixed_status_changed_count = 0 + lettings_postcode_not_fixed_status_changed_count = 0 + lettings_postcode_fixed_status_changed_ids = [] + lettings_postcode_not_fixed_status_changed_ids = [] + lettings_updated_without_issue = 0 + + updated_sales_logs_count = 0 + sales_postcode_fixed_count = 0 + sales_postcode_fixed_status_changed_count = 0 + sales_postcode_not_fixed_status_changed_count = 0 + sales_postcode_fixed_status_changed_ids = [] + sales_postcode_not_fixed_status_changed_ids = [] + sales_updated_without_issue = 0 + + lettings_logs = LettingsLog.filter_by_year(2024) + .where(status: %w[in_progress completed]) + .where(needstype: 1, manual_address_entry_selected: false, uprn: nil) + .where("(address_line1 IS NOT NULL AND address_line1 != '') OR (address_line2 IS NOT NULL AND address_line2 != '') OR (town_or_city IS NOT NULL AND town_or_city != '') OR (county IS NOT NULL AND county != '') OR (postcode_full IS NOT NULL AND postcode_full != '')") + + lettings_logs.find_each do |log| + status_pre_change = log.status + log.manual_address_entry_selected = true + if log.save + updated_lettings_logs_count += 1 + Rails.logger.info "manual_address_entry_selected updated for lettings log #{log.id}" + else + Rails.logger.info "Could not save manual_address_entry_selected changes to lettings log #{log.id} : #{log.errors.full_messages.join(', ')}" + end + + postcode_fixed = false + if log.postcode_full.nil? && log.address_line1 == log.address_line1_input + log.postcode_full = log.postcode_full_input + if log.save + lettings_postcode_fixed_count += 1 + Rails.logger.info "postcode_full updated by address_line1_input for lettings log #{log.id}" + postcode_fixed = true + else + Rails.logger.info "Could not save postcode_full changes to lettings log #{log.id} : #{log.errors.full_messages.join(', ')}" + end + end + + if log.postcode_full.nil? && log.creation_method == "bulk upload" && log.address_line1 == log.address_line1_as_entered + log.postcode_full = log.postcode_full_as_entered + if log.save + lettings_postcode_fixed_count += 1 + Rails.logger.info "postcode_full updated by address_line1_as_entered for lettings log #{log.id}" + postcode_fixed = true + else + Rails.logger.info "Could not save postcode_full changes to lettings log #{log.id} : #{log.errors.full_messages.join(', ')}" + end + end + + status_post_change = log.status + if status_pre_change != status_post_change + if postcode_fixed + lettings_postcode_fixed_status_changed_count += 1 + lettings_postcode_fixed_status_changed_ids << log.id + else + lettings_postcode_not_fixed_status_changed_count += 1 + lettings_postcode_not_fixed_status_changed_ids << log.id + end + else + lettings_updated_without_issue += 1 + end + end + + sales_logs = SalesLog.filter_by_year(2024) + .where(status: %w[in_progress completed]) + .where(manual_address_entry_selected: false, uprn: nil) + .where("(address_line1 IS NOT NULL AND address_line1 != '') OR (address_line2 IS NOT NULL AND address_line2 != '') OR (town_or_city IS NOT NULL AND town_or_city != '') OR (county IS NOT NULL AND county != '') OR (postcode_full IS NOT NULL AND postcode_full != '')") + + sales_logs.find_each do |log| + status_pre_change = log.status + log.manual_address_entry_selected = true + if log.save + updated_sales_logs_count += 1 + Rails.logger.info "manual_address_entry_selected updated for sales log #{log.id}" + else + Rails.logger.info "Could not save manual_address_entry_selected changes to sales log #{log.id} : #{log.errors.full_messages.join(', ')}" + end + + postcode_fixed = false + if log.postcode_full.nil? && log.address_line1 == log.address_line1_input + log.postcode_full = log.postcode_full_input + if log.save + sales_postcode_fixed_count += 1 + Rails.logger.info "postcode_full updated by address_line1_input for sales log #{log.id}" + postcode_fixed = true + else + Rails.logger.info "Could not save postcode_full changes to sales log #{log.id} : #{log.errors.full_messages.join(', ')}" + end + end + + if log.postcode_full.nil? && log.creation_method == "bulk upload" && log.address_line1 == log.address_line1_as_entered + log.postcode_full = log.postcode_full_as_entered + if log.save + sales_postcode_fixed_count += 1 + Rails.logger.info "postcode_full updated by address_line1_as_entered for sales log #{log.id}" + postcode_fixed = true + else + Rails.logger.info "Could not save postcode_full changes to sales log #{log.id} : #{log.errors.full_messages.join(', ')}" + end + end + + status_post_change = log.status + if status_pre_change != status_post_change + if postcode_fixed + sales_postcode_fixed_status_changed_count += 1 + sales_postcode_fixed_status_changed_ids << log.id + else + sales_postcode_not_fixed_status_changed_count += 1 + sales_postcode_not_fixed_status_changed_ids << log.id + end + else + sales_updated_without_issue += 1 + end + end + + puts "#{updated_lettings_logs_count} lettings logs were updated." + puts "#{lettings_updated_without_issue} lettings logs were updated without issue." + puts "#{lettings_postcode_fixed_count} lettings logs where postcode fix was applied." + puts "#{lettings_postcode_fixed_status_changed_count} lettings logs with postcode fix and status changed." + puts "#{lettings_postcode_not_fixed_status_changed_count} lettings logs without postcode fix and status changed." + puts "IDs of lettings logs with postcode fix and status changed: [#{lettings_postcode_fixed_status_changed_ids.join(', ')}]" + puts "IDs of lettings logs without postcode fix and status changed: [#{lettings_postcode_not_fixed_status_changed_ids.join(', ')}]" + + lettings_postcode_fixed_org_counts = LettingsLog.where(id: lettings_postcode_fixed_status_changed_ids).group(:owning_organisation_id).count + lettings_postcode_fixed_org_counts.each do |org_id, count| + puts "Org #{org_id}: #{count} logs with postcode fix and status changed." + end + + lettings_postcode_not_fixed_org_counts = LettingsLog.where(id: lettings_postcode_not_fixed_status_changed_ids).group(:owning_organisation_id).count + lettings_postcode_not_fixed_org_counts.each do |org_id, count| + puts "Org #{org_id}: #{count} logs without postcode fix and status changed." + end + + puts "#{updated_sales_logs_count} sales logs were updated." + puts "#{sales_updated_without_issue} sales logs were updated without issue." + puts "#{sales_postcode_fixed_count} sales logs where postcode fix was applied." + puts "#{sales_postcode_fixed_status_changed_count} sales logs with postcode fix and status changed." + puts "#{sales_postcode_not_fixed_status_changed_count} sales logs without postcode fix and status changed." + puts "IDs of sales logs with postcode fix and status changed: [#{sales_postcode_fixed_status_changed_ids.join(', ')}]" + puts "IDs of sales logs without postcode fix and status changed: [#{sales_postcode_not_fixed_status_changed_ids.join(', ')}]" + + sales_postcode_fixed_org_counts = SalesLog.where(id: sales_postcode_fixed_status_changed_ids).group(:owning_organisation_id).count + sales_postcode_fixed_org_counts.each do |org_id, count| + puts "Org #{org_id}: #{count} logs with postcode fix and status changed." + end + + sales_postcode_not_fixed_org_counts = SalesLog.where(id: sales_postcode_not_fixed_status_changed_ids).group(:owning_organisation_id).count + sales_postcode_not_fixed_org_counts.each do |org_id, count| + puts "Org #{org_id}: #{count} logs without postcode fix and status changed." + end + end + + desc "Find logs to fix and update postcode_full if conditions are met" + task update_postcode_full_preexisting_manual_entry_logs: :environment do + updated_count = 0 + fixed_count = 0 + not_updated_count = 0 + not_updated_ids = [] + updated_but_not_fixed_ids = [] + + logs_to_fix = LettingsLog.filter_by_year(2024).where(manual_address_entry_selected: true, uprn: nil, status: "in_progress", postcode_full: nil, updated_at: Time.zone.parse("2025-03-19 16:00:00")..Time.zone.parse("2025-03-19 17:00:00")) + + logs_to_fix.find_each do |log| + previous_version = log.versions[-2] + previous_status = previous_version&.reify&.status + + if log.address_line1 == log.address_line1_input + log.postcode_full = log.postcode_full_input + elsif log.creation_method == "bulk upload" && log.address_line1 == log.address_line1_as_entered + log.postcode_full = log.postcode_full_as_entered + end + + if log.postcode_full.present? + if log.save + Rails.logger.info "Updated postcode_full for lettings log #{log.id}" + updated_count += 1 + if log.status == previous_status + fixed_count += 1 + else + updated_but_not_fixed_ids << log.id + end + else + Rails.logger.info "Could not save changes to lettings log #{log.id}: #{log.errors.full_messages.join(', ')}" + not_updated_count += 1 + not_updated_ids << log.id + end + else + not_updated_count += 1 + not_updated_ids << log.id + end + end + + puts "#{updated_count} logs updated." + puts "#{fixed_count} logs fixed." + puts "#{not_updated_count} logs not updated." + puts "IDs of logs not updated: [#{not_updated_ids.join(', ')}]" + puts "IDs of logs updated but not fixed: [#{updated_but_not_fixed_ids.join(', ')}]" + end +end diff --git a/spec/factories/lettings_log.rb b/spec/factories/lettings_log.rb index ad81bca5d..62cc94a14 100644 --- a/spec/factories/lettings_log.rb +++ b/spec/factories/lettings_log.rb @@ -172,7 +172,7 @@ FactoryBot.define do if log.startdate >= Time.zone.local(2024, 4, 1) log.nationality_all_group = 826 log.uprn = evaluator.uprn || "10033558653" - log.uprn_selection = evaluator.uprn_selection || "10033558653" + log.uprn_selection = evaluator.uprn_selection || evaluator.uprn || "10033558653" end end end @@ -220,5 +220,38 @@ FactoryBot.define do instance.save!(validate: false) end end + trait :completed_without_uprn do + completed + manual_address_entry_selected { false } + after(:build) do |log| + log.uprn = nil + log.uprn_selection = nil + log.uprn_known = 0 + end + + after(:build) do |log| + log.address_line1 = "1 Test Street" + log.address_line2 = "Testville" + log.county = "Testshire" + log.town_or_city = "Testford" + log.postcode_full = "SW1 1AA" + end + end + trait :inprogress_without_address_fields do + completed + manual_address_entry_selected { false } + after(:build) do |log| + log.uprn = nil + log.uprn_selection = nil + log.uprn_known = 0 + log.address_line1 = nil + log.address_line2 = nil + log.county = nil + log.town_or_city = nil + log.postcode_full = nil + log.address_line1_input = nil + log.postcode_full_input = nil + end + end end end diff --git a/spec/factories/sales_log.rb b/spec/factories/sales_log.rb index 64137704c..a2e3f7d4c 100644 --- a/spec/factories/sales_log.rb +++ b/spec/factories/sales_log.rb @@ -173,7 +173,7 @@ FactoryBot.define do log.nationality_all_group = 826 log.nationality_all_buyer2_group = 826 log.uprn = evaluator.uprn || "10033558653" - log.uprn_selection = evaluator.uprn_selection || "10033558653" + log.uprn_selection = evaluator.uprn_selection || evaluator.uprn || "10033558653" end if log.saledate >= Time.zone.local(2025, 4, 1) log.relat2 = "X" if log.relat2 == "C" @@ -184,6 +184,39 @@ FactoryBot.define do end end end + trait :discounted_2025_completed do + completed + discount { 20 } + end + trait :shared_2025_completed do + completed + shared_ownership + staircase { 2 } + resale { 2 } + frombeds { 2 } + fromprop { 1 } + socprevten { 3 } + mrent { 900 } + equity { 30 } + ppostcode_full { "SW1A 1AA" } + hodate { Time.zone.today } + end + trait :staircasing_2025_completed do + completed + shared_ownership + staircase { 1 } + stairbought { 10 } + stairowned { 60 } + staircasesale { 2 } + firststair { 2 } + initialpurchase { Time.zone.today - 2.years } + numstair { 2 } + lasttransaction { Time.zone.today - 1.year } + mrentprestaircasing { 1000 } + mrent { 900 } + equity { 30 } + ppostcode_full { "SW1A 1AA" } + end trait :with_uprn do uprn { rand(999_999_999_999).to_s } uprn_known { 1 } @@ -202,5 +235,137 @@ FactoryBot.define do instance.save!(validate: false) end end + trait :completed_without_uprn do + completed + manual_address_entry_selected { false } + after(:build) do |log| + log.uprn = nil + log.uprn_selection = nil + log.uprn_known = 0 + end + + after(:build) do |log| + log.address_line1 = "1 Test Street" + log.address_line2 = "Testville" + log.county = "Testshire" + log.town_or_city = "Testford" + log.postcode_full = "SW1 1AA" + end + end + trait :inprogress_without_address_fields do + completed + manual_address_entry_selected { false } + after(:build) do |log| + log.uprn = nil + log.uprn_selection = nil + log.uprn_known = 0 + log.address_line1 = nil + log.address_line2 = nil + log.county = nil + log.town_or_city = nil + log.postcode_full = nil + log.address_line1_input = nil + log.postcode_full_input = nil + end + end + trait :export do + purchid { "123" } + ownershipsch { 2 } + type { 8 } + saledate_today + jointpur { 1 } + beds { 2 } + jointmore { 1 } + noint { 2 } + privacynotice { 1 } + age1_known { 0 } + age1 { 27 } + sex1 { "F" } + national { 18 } + buy1livein { 1 } + relat2 { "P" } + proptype { 1 } + age2_known { 0 } + age2 { 33 } + builtype { 1 } + ethnic { 3 } + ethnic_group { 17 } + sex2 { "X" } + buy2livein { "1" } + ecstat1 { "1" } + ecstat2 { "1" } + hholdcount { "4" } + wheel { 1 } + details_known_3 { 1 } + age3_known { 0 } + age3 { 14 } + details_known_4 { 1 } + age4_known { 0 } + age4 { 18 } + details_known_5 { 1 } + age5_known { 0 } + age5 { 40 } + details_known_6 { 1 } + age6_known { 0 } + age6 { 40 } + income1nk { 0 } + income1 { 10_000 } + inc1mort { 1 } + income2nk { 0 } + income2 { 10_000 } + inc2mort { 1 } + uprn_known { 0 } + address_line1 { "Address line 1" } + town_or_city { "City" } + la_known { 1 } + la { "E09000003" } + savingsnk { 1 } + prevown { 1 } + prevshared { 2 } + sex3 { "F" } + sex4 { "X" } + sex5 { "M" } + sex6 { "X" } + mortgage { 20_000 } + ecstat3 { 9 } + ecstat4 { 3 } + ecstat5 { 2 } + ecstat6 { 1 } + disabled { 1 } + deposit { 80_000 } + value { 110_000 } + value_value_check { 0 } + grant { 10_000 } + hhregres { 7 } + ppcodenk { 1 } + prevten { 1 } + previous_la_known { 0 } + relat3 { "X" } + relat4 { "X" } + relat5 { "R" } + relat6 { "R" } + hb { 4 } + mortgageused { 1 } + wchair { 1 } + armedforcesspouse { 5 } + has_mscharge { 1 } + mscharge { 100 } + mortlen { 10 } + pcodenk { 0 } + postcode_full { "SW1A 1AA" } + is_la_inferred { false } + mortgagelender { 5 } + extrabor { 1 } + ethnic_group2 { 17 } + nationalbuy2 { 13 } + buy2living { 3 } + proplen_asked { 1 } + address_line1_input { "Address line 1" } + postcode_full_input { "SW1A 1AA" } + nationality_all_group { 826 } + nationality_all_buyer2_group { 826 } + uprn { "10033558653" } + uprn_selection { 1 } + end end end diff --git a/spec/fixtures/exports/general_needs_log_25_26.xml b/spec/fixtures/exports/general_needs_log_25_26.xml new file mode 100644 index 000000000..57caaad28 --- /dev/null +++ b/spec/fixtures/exports/general_needs_log_25_26.xml @@ -0,0 +1,176 @@ + + +
+ 2 + BZ737 + 35 + F + 2 + 6 + 0 + 2 + 32 + M + 6 + + + + + + + + + + + + + + + + + + + 1 + 4 + 4 + 1 + 1 + 2 + 1 + 5 + 4 + A1 1AA + 6 + 7 + 3 + 1 + 268 + 1 + 1 + 2 + 2 + 7 + AA1 1AA + 1 + 0 + 1 + 0 + + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + + + + 4 + 123 + E09000033 + E07000105 + 6 + 1 + 2022-05-05T10:36:49+01:00 + 0 + 2025-04-03T00:00:00+01:00 + 1 + 2 + 1 + 2021-11-03T00:00:00+00:00 + 2 + 1 + 7 + 0 + 0 + 2 + 0 + 2 + 200.0 + 50.0 + 40.0 + 35.0 + 325.0 + 12.0 + 0 + 1 + 0 + 100.0 + 25.0 + 20.0 + 17.5 + 162.5 + 6.0 + 0 + 1 + + 2 + P + + + + + + + + 2 + + 3 + + 4 + 2 + 1064 + 1 + 1 + 1 + 1 + 1, Test Street + + Test Town + + + 2 + + + + + 0 + 826 + address line 1 as entered + address line 2 as entered + town or city as entered + county as entered + AB1 2CD + la as entered + {id} + {owning_org_id} + {owning_org_name} + 1234 + {managing_org_id} + {managing_org_name} + 1234 + 2025-04-03T00:00:00+01:00 + 2025-04-03T00:00:00+01:00 + {log_id} + test1@example.com + test1@example.com + + 2 + 1 + +
diff --git a/spec/fixtures/exports/sales_log.xml b/spec/fixtures/exports/sales_log.xml new file mode 100644 index 000000000..37dc6dda5 --- /dev/null +++ b/spec/fixtures/exports/sales_log.xml @@ -0,0 +1,154 @@ + + +
+ {id} + 1 + 123 + 8 + 1 + 2 + 27 + F + 17 + 1 + 1 + 33 + P + X + 2 + 1 + 1 + 1 + 1 + 4 + 14 + E09000033 + 10000 + 18 + 40 + 40 + 1 + 10000 + 1 + + 1 + F + 20000.0 + 1 + 9 + 3 + 2 + 1 + X + X + R + R + 4 + X + M + X + + + + + + + 80000.0 + + 1 + 110000.0 + + + 10000.0 + 0 + SW1A + 1AA + E09000033 + 7 + + + 100.0 + 1 + 1 + 1 + 5 + + + + + + 1 + 6 + + 2 + 3 + + + + + + + 1 + 3 + 2026 + 2026-03-01T00:00:00+00:00 + {created_by_email} + {created_by_id} + {assigned_to_email} + {assigned_to_id} + 2026-03-01T00:00:00+00:00 + + + {owning_org_id} + {owning_org_name} + {managing_org_id} + {managing_org_name} + 1 + + 2025 + 2 + 1 + 17 + 17 + 1 + 1 + + + 0 + 0 + SW1A 1AA + true + 10 + + + Address line 1 + + City + Westminster + Address line 1 + SW1A 1AA + + + + + + + + 826 + 826 + Westminster + 1 + 1 + + + + + + + + + + + + +
diff --git a/spec/fixtures/exports/sales_log_2024.xml b/spec/fixtures/exports/sales_log_2024.xml new file mode 100644 index 000000000..e894ac973 --- /dev/null +++ b/spec/fixtures/exports/sales_log_2024.xml @@ -0,0 +1,154 @@ + + +
+ {id} + 2 + 123 + 8 + 1 + 2 + 27 + F + 17 + 1 + 1 + 33 + P + X + 2 + 1 + 1 + 1 + 1 + 4 + 14 + E09000033 + 10000 + 18 + 40 + 40 + 1 + 10000 + 1 + + 1 + F + 20000.0 + 1 + 9 + 3 + 2 + 1 + X + X + R + R + 4 + X + M + X + + + + + + + 80000.0 + + 1 + 110000.0 + + + 10000.0 + 0 + SW1A + 1AA + E09000033 + 7 + + + 100.0 + 1 + 1 + 1 + 5 + + + + + + 1 + 6 + + 2 + 3 + + + + + + + 3 + 4 + 2024 + 2024-04-03T00:00:00+01:00 + {created_by_email} + {created_by_id} + {assigned_to_email} + {assigned_to_id} + 2024-04-03T00:00:00+01:00 + + + {owning_org_id} + {owning_org_name} + {managing_org_id} + {managing_org_name} + 1 + + 2024 + 2 + 1 + 17 + 17 + 1 + 1 + + + 0 + 0 + SW1A 1AA + true + 10 + + + Address line 1 + + City + Westminster + Address line 1 + SW1A 1AA + + + + + + + + 826 + 826 + Westminster + 1 + 1 + + + + + + + + + + + + +
diff --git a/spec/fixtures/files/lettings_log_csv_export_codes_25.csv b/spec/fixtures/files/lettings_log_csv_export_codes_25.csv new file mode 100644 index 000000000..8ef206ca0 --- /dev/null +++ b/spec/fixtures/files/lettings_log_csv_export_codes_25.csv @@ -0,0 +1,3 @@ +Log ID,Status of log,ID of a set of duplicate logs,User the log is created by,User the log is assigned to,Is the user in the assigned_to column the data protection officer?,Time and date the log was created,User who last updated the log,Time and date the log was last updated,Was the log submitted in-service or via bulk upload?,Year collection period opened,ID of a set of bulk uploaded logs,Which organisation owns this property?,Which organisation manages this letting?,What is the needs type?,What is the letting type?,Is this letting a renewal?,What is the tenancy start date?,"What is the rent type? (grouped into SR, IR or AR)",What is the rent type?,Which type of Intermediate Rent is this letting?,Which 'Other' type of Intermediate Rent is this letting?,Is this a London Affordable Rent letting?,What is the tenant code?,What is the property reference?,Has the tenant seen the MHCLG privacy notice?,Is this the first time the property has been let as social housing?,What rent product was the property most recently let as?,What is the reason for the property being vacant?,Is this property new to the social rented sector?,"If known, property's UPRN",Address line 1,Address line 2,Town or City,County,Postcode,The internal value to indicate if the LA was inferred from the postcode,What is the property's local authority?,Local authority code,Is the UPRN known?,UPRN of the address selected,Was the 'No address found' page seen?,Address line 1 input from address matching feature,Postcode input from address matching feature,Address line 1 entered in bulk upload file,Address line 2 entered in bulk upload file,Town or city entered in bulk upload file,County entered in bulk upload file,Postcode entered in bulk upload file,Local authority entered in bulk upload file,What type of unit is the property?,Which type of building is the property?,Is the property built or adapted to wheelchair-user standards?,How many bedrooms does the property have?,What is the void date?,Number of days the property was vacant,The following soft validation was confirmed: You told us that the property has been vacant for more than 2 years. This is higher than we would expect.,Were any major repairs carried out during the void period?,What date were any major repairs completed on?,The following soft validation was confirmed: You told us the property has been vacant for 2 years. This is higher than we would expect.,Is this letting in sheltered accommodation?,Is this a joint tenancy?,Is this a starter tenancy?,What is the type of tenancy?,"If 'Other', what is the type of tenancy?",What is the length of the fixed-term tenancy to the nearest year?,How many people live in the household at this letting?,The following soft validation was confirmed: You told us somebody in the household is pregnant. You also told us there are no female tenants living at the property.,"Where household characteristics have a 'Refused' option for some or all of: AGE1-AGE8, SEX1-SEX8, RELAT2-RELAT8, ECSTAT1-ECSTAT8","Type of household 1 = 1 elder; 2 = 2 adults, including elder(s); 3 = 1 adult; 4 = 2 adults; 5 = 1 adult & 1+ children; 6 = 2+ adults & 1+ children; 9 = Other",Total number of dependent children in the household (Sum of when RELAT2-8 = C),Total number of elders in household (Sum of when AGE1-8 >= 60),Total number of adults in household,What is the lead tenant's age?,The following soft validation was confirmed: You told us this person is aged %{age} years and retired. The minimum expected retirement age for %{gender} in England is %{age}.,Which of these best describes the lead tenant's gender identity?,What is the lead tenant's ethnic group?,Which of these best describes the lead tenant's ethnic background?,What is the lead tenant's nationality?,Which of these best describes the lead tenant's working situation?,Are the details of tenant 2 known?,What is person 2's relationship to the lead tenant?,"The following soft validation was confirmed: You said that [person X]'s relationship to lead tenant is partner, and that their age is [AGEX]. Are you sure this is correct?",The following soft validation was confirmed: You said that more than one person in the household is the partner of the lead tenant. Are you sure this is correct?,What is person 2's age?,Which of these best describes person 2's gender identity?,Which of these best describes person 2's working situation?,Are the details of tenant 3 known?,What is person 3's relationship to the lead tenant?,What is person 3's age?,Which of these best describes person 3's gender identity?,Which of these best describes person 3's working situation?,Are the details of tenant 4 known?,What is person 4's relationship to the lead tenant?,What is person 4's age?,Which of these best describes person 4's gender identity?,Which of these best describes person 4's working situation?,Are the details of tenant 5 known?,What is person 5's relationship to the lead tenant?,What is person 5's age?,Which of these best describes person 5's gender identity?,Which of these best describes person 5's working situation?,Are the details of tenant 6 known?,What is person 6's relationship to the lead tenant?,What is person 6's age?,Which of these best describes person 6's gender identity?,Which of these best describes person 6's working situation?,Are the details of tenant 7 known?,What is person 7's relationship to the lead tenant?,What is person 7's age?,Which of these best describes person 7's gender identity?,Which of these best describes person 7's working situation?,Are the details of tenant 8 known?,What is person 8's relationship to the lead tenant?,What is person 8's age?,Which of these best describes person 8's gender identity?,Which of these best describes person 8's working situation?,Does anybody in the household have links to the UK armed forces?,Is this person still serving in the UK armed forces?,Was this person seriously injured or ill as a result of serving in the UK armed forces?,Is anybody in the household pregnant?,Does anybody in the household have any disabled access needs?,"What access needs do they have? (Fully wheelchair-accessible housing, Level access housing or Wheelchair access to essential rooms)",Disabled access needs a) Fully wheelchair-accessible housing,Disabled access needs b) Wheelchair access to essential rooms,Disabled access needs c) Level access housing,Disabled access needs f) Other disabled access needs,Disabled access needs g) No disabled access needs,Disabled access needs h) Don't know,Do they have any other disabled access needs?,Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more?,Does this person's condition affect their dexterity?,Does this person's condition affect their learning or understanding or concentrating?,Does this person's condition affect their hearing?,Does this person's condition affect their memory?,Does this person's condition affect their mental health?,Does this person's condition affect their mobility?,Does this person's condition affect them socially or behaviourally?,Does this person's condition affect their stamina or breathing or fatigue?,Does this person's condition affect their vision?,Does this person's condition affect them in another way?,How long has the household continuously lived in the local authority area of the new letting?,How long has the household been on the local authority waiting list for the new letting?,What is the tenant's main reason for the household leaving their last settled home?,"If 'Other', what was the main reason for leaving their last settled home?",The soft validation was confirmed,Where was the household immediately before this letting?,Did the household experience homelessness immediately before this letting?,Previous postcode unknown or previous accommodation was temporary,What is the postcode of the household's last settled home?,Was the local authority of the household's last settled home known?,The internal value to indicate if the previous LA was inferred from the postcode,Previous location LA name,Previous location's ONS LA Code,Was the household given reasonable preference by the local authority?,Reasonable preference reason - They were homeless or about to lose their home (within 56 days),"Reasonable preference reason - They were living in insanitary, overcrowded or unisatisfactory housing",Reasonable preference reason - They needed to move on medical and welfare reasons (including disability),Reasonable preference reason - They needed to move to avoid hardship to themselves or others,Reasonable preference reason - Don't Know,Was the letting made under Choice-Based Lettings (CBL)?,Was the letting made under the Common Allocation Policy (CAP)?,Was the letting made under the Common Housing Register (CHR)?,Was the letting made under the Accessible Register?,"The letting was not allocated under CBL, CAP, CHR or Accessible Register.",,What was the source of referral for this letting?,"The following soft validation was confirmed: Are you sure? This is a general needs log, and this referral type is for supported housing.",Do you know the household's combined income after tax?,Was the household income refused?,How often does the household receive income?,How much income does the household have in total?,Populated when someone hits the soft validation and confirmed in the service,Is the tenant likely to be receiving any of these housing-related benefits?,"Does the tenant receive housing-related benefits? Yes if hb = Universal Credit housing element or Housing benefit, No if hb = Don't Know, Neither, Tenant prefers not to say or blank","How much of the household's income is from Universal Credit, state pensions or benefits?",Does the household pay rent or other charges for the accommodation?,Does the household pay rent or other charges for the accommodation? - flag for when household_charge is answered no,How often does the household pay rent and other charges?,What is the basic rent?,Weekly rent,Populated when the soft validation and confirmed in the service,What is the service charge?,Weekly service charge,What is the personal service charge?,Weekly personal service charge,What is the support charge?,Weekly support charge,Total charge to the tenant,Weekly total charge to the tenant,Populated when the soft validation and confirmed in the service,Populated when the soft validation and confirmed in the service,Populated when the soft validation and confirmed in the service,"After the household has received any housing-related benefits, will they still need to pay for rent and charges?",Can you estimate the outstanding amount?,Estimated outstanding amount,Weekly total rent shortfall charge for tenant receiving housing benefit,What scheme does this letting belong to?,"From scheme code, we map to the scheme name",Does the scheme contain confidential information?,"What is this type of scheme? (Direct access hostel), Foyer, Housing for older people or Other supported housing",Is this scheme registered under the Care Standards Act 2000?,Which organisation owns the housing stock for this scheme?,What client group is this scheme intended for?,Does this scheme provide for another client group?,What is the other client group?,What support does this scheme provide?,Intended length of stay,Date scheme was created,Which location is this letting for?,What is the postcode for this location?,What is the name of this location?,How many units are at this location?,What is the most common type of unit at this location?,What are the mobility standards for the majority of the units in this location?,What is the local authority of this postcode?,When did the first property in this location become available under this scheme? +id,status,duplicate_set_id,created_by,assigned_to,is_dpo,created_at,updated_by,updated_at,creation_method,collection_start_year,bulk_upload_id,owning_organisation_name,managing_organisation_name,needstype,lettype,renewal,startdate,renttype,renttype_detail,irproduct,irproduct_other,lar,tenancycode,propcode,declaration,first_time_property_let_as_social_housing,unitletas,rsnvac,newprop,uprn,address_line1,address_line2,town_or_city,county,postcode_full,is_la_inferred,la_label,la,uprn_known,uprn_selection,address_search_value_check,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,unittype_gn,builtype,wchair,beds,voiddate,vacdays,void_date_value_check,majorrepairs,mrcdate,major_repairs_date_value_check,sheltered,joint,startertenancy,tenancy,tenancyother,tenancylength,hhmemb,pregnancy_value_check,refused,hhtype,totchild,totelder,totadult,age1,retirement_value_check,sex1,ethnic_group,ethnic,nationality_all,ecstat1,details_known_2,relat2,partner_under_16_value_check,multiple_partners_value_check,age2,sex2,ecstat2,details_known_3,relat3,age3,sex3,ecstat3,details_known_4,relat4,age4,sex4,ecstat4,details_known_5,relat5,age5,sex5,ecstat5,details_known_6,relat6,age6,sex6,ecstat6,details_known_7,relat7,age7,sex7,ecstat7,details_known_8,relat8,age8,sex8,ecstat8,armedforces,leftreg,reservist,preg_occ,housingneeds,housingneeds_type,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_f,housingneeds_g,housingneeds_h,housingneeds_other,illness,illness_type_4,illness_type_5,illness_type_2,illness_type_6,illness_type_7,illness_type_3,illness_type_9,illness_type_8,illness_type_1,illness_type_10,layear,waityear,reason,reasonother,reasonother_value_check,prevten,homeless,ppcodenk,ppostcode_full,previous_la_known,is_previous_la_inferred,prevloc_label,prevloc,reasonpref,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,cbl,cap,chr,accessible_register,letting_allocation_none,referral_type,referral,referral_value_check,net_income_known,incref,incfreq,earnings,net_income_value_check,hb,has_benefits,benefits,household_charge,nocharge,period,brent,wrent,rent_value_check,scharge,wscharge,pscharge,wpschrge,supcharg,wsupchrg,tcharge,wtcharge,scharge_value_check,pscharge_value_check,supcharg_value_check,hbrentshortfall,tshortfall_known,tshortfall,wtshortfall,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 +,in_progress,,s.port@jeemayle.com,s.port@jeemayle.com,false,2025-04-01T00:00:00+01:00,,2025-04-01T00:00:00+01:00,1,2025,,MHCLG,MHCLG,1,7,0,2025-04-01,2,2,,,2,HIJKLMN,ABCDEFG,1,0,2,6,2,,Address line 1,,London,,NW9 5LL,false,Barnet,E09000003,0,,,,,address line 1 as entered,address line 2 as entered,town or city as entered,county as entered,AB1 2CD,la as entered,7,1,1,3,2025-03-30,1,,1,2025-03-31,,,3,1,4,,2,4,,1,4,0,0,2,35,,F,0,2,36,0,0,P,,,32,M,6,1,R,-9,R,10,0,R,-9,R,10,,,,,,,,,,,,,,,,,,,,,1,4,1,2,1,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,2,7,4,,,6,1,0,TN23 6LZ,1,false,Ashford,E07000105,1,0,1,0,0,0,0,0,1,0,0,,,,0,0,1,268,,6,1,1,,0,2,200.0,100.0,,50.0,25.0,40.0,20.0,35.0,17.5,325.0,162.5,,,,1,0,12.0,6.0,,,,,,,,,,,,,,,,,,,, diff --git a/spec/fixtures/files/lettings_log_csv_export_labels_25.csv b/spec/fixtures/files/lettings_log_csv_export_labels_25.csv new file mode 100644 index 000000000..f5390d27f --- /dev/null +++ b/spec/fixtures/files/lettings_log_csv_export_labels_25.csv @@ -0,0 +1,3 @@ +Log ID,Status of log,ID of a set of duplicate logs,User the log is created by,User the log is assigned to,Is the user in the assigned_to column the data protection officer?,Time and date the log was created,User who last updated the log,Time and date the log was last updated,Was the log submitted in-service or via bulk upload?,Year collection period opened,ID of a set of bulk uploaded logs,Which organisation owns this property?,Which organisation manages this letting?,What is the needs type?,What is the letting type?,Is this letting a renewal?,What is the tenancy start date?,"What is the rent type? (grouped into SR, IR or AR)",What is the rent type?,Which type of Intermediate Rent is this letting?,Which 'Other' type of Intermediate Rent is this letting?,Is this a London Affordable Rent letting?,What is the tenant code?,What is the property reference?,Has the tenant seen the MHCLG privacy notice?,Is this the first time the property has been let as social housing?,What rent product was the property most recently let as?,What is the reason for the property being vacant?,Is this property new to the social rented sector?,"If known, property's UPRN",Address line 1,Address line 2,Town or City,County,Postcode,The internal value to indicate if the LA was inferred from the postcode,What is the property's local authority?,Local authority code,Is the UPRN known?,UPRN of the address selected,Was the 'No address found' page seen?,Address line 1 input from address matching feature,Postcode input from address matching feature,Address line 1 entered in bulk upload file,Address line 2 entered in bulk upload file,Town or city entered in bulk upload file,County entered in bulk upload file,Postcode entered in bulk upload file,Local authority entered in bulk upload file,What type of unit is the property?,Which type of building is the property?,Is the property built or adapted to wheelchair-user standards?,How many bedrooms does the property have?,What is the void date?,Number of days the property was vacant,The following soft validation was confirmed: You told us that the property has been vacant for more than 2 years. This is higher than we would expect.,Were any major repairs carried out during the void period?,What date were any major repairs completed on?,The following soft validation was confirmed: You told us the property has been vacant for 2 years. This is higher than we would expect.,Is this letting in sheltered accommodation?,Is this a joint tenancy?,Is this a starter tenancy?,What is the type of tenancy?,"If 'Other', what is the type of tenancy?",What is the length of the fixed-term tenancy to the nearest year?,How many people live in the household at this letting?,The following soft validation was confirmed: You told us somebody in the household is pregnant. You also told us there are no female tenants living at the property.,"Where household characteristics have a 'Refused' option for some or all of: AGE1-AGE8, SEX1-SEX8, RELAT2-RELAT8, ECSTAT1-ECSTAT8","Type of household 1 = 1 elder; 2 = 2 adults, including elder(s); 3 = 1 adult; 4 = 2 adults; 5 = 1 adult & 1+ children; 6 = 2+ adults & 1+ children; 9 = Other",Total number of dependent children in the household (Sum of when RELAT2-8 = C),Total number of elders in household (Sum of when AGE1-8 >= 60),Total number of adults in household,What is the lead tenant's age?,The following soft validation was confirmed: You told us this person is aged %{age} years and retired. The minimum expected retirement age for %{gender} in England is %{age}.,Which of these best describes the lead tenant's gender identity?,What is the lead tenant's ethnic group?,Which of these best describes the lead tenant's ethnic background?,What is the lead tenant's nationality?,Which of these best describes the lead tenant's working situation?,Are the details of tenant 2 known?,What is person 2's relationship to the lead tenant?,"The following soft validation was confirmed: You said that [person X]'s relationship to lead tenant is partner, and that their age is [AGEX]. Are you sure this is correct?",The following soft validation was confirmed: You said that more than one person in the household is the partner of the lead tenant. Are you sure this is correct?,What is person 2's age?,Which of these best describes person 2's gender identity?,Which of these best describes person 2's working situation?,Are the details of tenant 3 known?,What is person 3's relationship to the lead tenant?,What is person 3's age?,Which of these best describes person 3's gender identity?,Which of these best describes person 3's working situation?,Are the details of tenant 4 known?,What is person 4's relationship to the lead tenant?,What is person 4's age?,Which of these best describes person 4's gender identity?,Which of these best describes person 4's working situation?,Are the details of tenant 5 known?,What is person 5's relationship to the lead tenant?,What is person 5's age?,Which of these best describes person 5's gender identity?,Which of these best describes person 5's working situation?,Are the details of tenant 6 known?,What is person 6's relationship to the lead tenant?,What is person 6's age?,Which of these best describes person 6's gender identity?,Which of these best describes person 6's working situation?,Are the details of tenant 7 known?,What is person 7's relationship to the lead tenant?,What is person 7's age?,Which of these best describes person 7's gender identity?,Which of these best describes person 7's working situation?,Are the details of tenant 8 known?,What is person 8's relationship to the lead tenant?,What is person 8's age?,Which of these best describes person 8's gender identity?,Which of these best describes person 8's working situation?,Does anybody in the household have links to the UK armed forces?,Is this person still serving in the UK armed forces?,Was this person seriously injured or ill as a result of serving in the UK armed forces?,Is anybody in the household pregnant?,Does anybody in the household have any disabled access needs?,"What access needs do they have? (Fully wheelchair-accessible housing, Level access housing or Wheelchair access to essential rooms)",Disabled access needs a) Fully wheelchair-accessible housing,Disabled access needs b) Wheelchair access to essential rooms,Disabled access needs c) Level access housing,Disabled access needs f) Other disabled access needs,Disabled access needs g) No disabled access needs,Disabled access needs h) Don't know,Do they have any other disabled access needs?,Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more?,Does this person's condition affect their dexterity?,Does this person's condition affect their learning or understanding or concentrating?,Does this person's condition affect their hearing?,Does this person's condition affect their memory?,Does this person's condition affect their mental health?,Does this person's condition affect their mobility?,Does this person's condition affect them socially or behaviourally?,Does this person's condition affect their stamina or breathing or fatigue?,Does this person's condition affect their vision?,Does this person's condition affect them in another way?,How long has the household continuously lived in the local authority area of the new letting?,How long has the household been on the local authority waiting list for the new letting?,What is the tenant's main reason for the household leaving their last settled home?,"If 'Other', what was the main reason for leaving their last settled home?",The soft validation was confirmed,Where was the household immediately before this letting?,Did the household experience homelessness immediately before this letting?,Previous postcode unknown or previous accommodation was temporary,What is the postcode of the household's last settled home?,Was the local authority of the household's last settled home known?,The internal value to indicate if the previous LA was inferred from the postcode,Previous location LA name,Previous location's ONS LA Code,Was the household given reasonable preference by the local authority?,Reasonable preference reason - They were homeless or about to lose their home (within 56 days),"Reasonable preference reason - They were living in insanitary, overcrowded or unisatisfactory housing",Reasonable preference reason - They needed to move on medical and welfare reasons (including disability),Reasonable preference reason - They needed to move to avoid hardship to themselves or others,Reasonable preference reason - Don't Know,Was the letting made under Choice-Based Lettings (CBL)?,Was the letting made under the Common Allocation Policy (CAP)?,Was the letting made under the Common Housing Register (CHR)?,Was the letting made under the Accessible Register?,"The letting was not allocated under CBL, CAP, CHR or Accessible Register.",,What was the source of referral for this letting?,"The following soft validation was confirmed: Are you sure? This is a general needs log, and this referral type is for supported housing.",Do you know the household's combined income after tax?,Was the household income refused?,How often does the household receive income?,How much income does the household have in total?,Populated when someone hits the soft validation and confirmed in the service,Is the tenant likely to be receiving any of these housing-related benefits?,"Does the tenant receive housing-related benefits? Yes if hb = Universal Credit housing element or Housing benefit, No if hb = Don't Know, Neither, Tenant prefers not to say or blank","How much of the household's income is from Universal Credit, state pensions or benefits?",Does the household pay rent or other charges for the accommodation?,Does the household pay rent or other charges for the accommodation? - flag for when household_charge is answered no,How often does the household pay rent and other charges?,What is the basic rent?,Weekly rent,Populated when the soft validation and confirmed in the service,What is the service charge?,Weekly service charge,What is the personal service charge?,Weekly personal service charge,What is the support charge?,Weekly support charge,Total charge to the tenant,Weekly total charge to the tenant,Populated when the soft validation and confirmed in the service,Populated when the soft validation and confirmed in the service,Populated when the soft validation and confirmed in the service,"After the household has received any housing-related benefits, will they still need to pay for rent and charges?",Can you estimate the outstanding amount?,Estimated outstanding amount,Weekly total rent shortfall charge for tenant receiving housing benefit,What scheme does this letting belong to?,"From scheme code, we map to the scheme name",Does the scheme contain confidential information?,"What is this type of scheme? (Direct access hostel), Foyer, Housing for older people or Other supported housing",Is this scheme registered under the Care Standards Act 2000?,Which organisation owns the housing stock for this scheme?,What client group is this scheme intended for?,Does this scheme provide for another client group?,What is the other client group?,What support does this scheme provide?,Intended length of stay,Date scheme was created,Which location is this letting for?,What is the postcode for this location?,What is the name of this location?,How many units are at this location?,What is the most common type of unit at this location?,What are the mobility standards for the majority of the units in this location?,What is the local authority of this postcode?,When did the first property in this location become available under this scheme? +id,status,duplicate_set_id,created_by,assigned_to,is_dpo,created_at,updated_by,updated_at,creation_method,collection_start_year,bulk_upload_id,owning_organisation_name,managing_organisation_name,needstype,lettype,renewal,startdate,renttype,renttype_detail,irproduct,irproduct_other,lar,tenancycode,propcode,declaration,first_time_property_let_as_social_housing,unitletas,rsnvac,newprop,uprn,address_line1,address_line2,town_or_city,county,postcode_full,is_la_inferred,la_label,la,uprn_known,uprn_selection,address_search_value_check,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,unittype_gn,builtype,wchair,beds,voiddate,vacdays,void_date_value_check,majorrepairs,mrcdate,major_repairs_date_value_check,sheltered,joint,startertenancy,tenancy,tenancyother,tenancylength,hhmemb,pregnancy_value_check,refused,hhtype,totchild,totelder,totadult,age1,retirement_value_check,sex1,ethnic_group,ethnic,nationality_all,ecstat1,details_known_2,relat2,partner_under_16_value_check,multiple_partners_value_check,age2,sex2,ecstat2,details_known_3,relat3,age3,sex3,ecstat3,details_known_4,relat4,age4,sex4,ecstat4,details_known_5,relat5,age5,sex5,ecstat5,details_known_6,relat6,age6,sex6,ecstat6,details_known_7,relat7,age7,sex7,ecstat7,details_known_8,relat8,age8,sex8,ecstat8,armedforces,leftreg,reservist,preg_occ,housingneeds,housingneeds_type,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_f,housingneeds_g,housingneeds_h,housingneeds_other,illness,illness_type_4,illness_type_5,illness_type_2,illness_type_6,illness_type_7,illness_type_3,illness_type_9,illness_type_8,illness_type_1,illness_type_10,layear,waityear,reason,reasonother,reasonother_value_check,prevten,homeless,ppcodenk,ppostcode_full,previous_la_known,is_previous_la_inferred,prevloc_label,prevloc,reasonpref,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,cbl,cap,chr,accessible_register,letting_allocation_none,referral_type,referral,referral_value_check,net_income_known,incref,incfreq,earnings,net_income_value_check,hb,has_benefits,benefits,household_charge,nocharge,period,brent,wrent,rent_value_check,scharge,wscharge,pscharge,wpschrge,supcharg,wsupchrg,tcharge,wtcharge,scharge_value_check,pscharge_value_check,supcharg_value_check,hbrentshortfall,tshortfall_known,tshortfall,wtshortfall,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 +,in_progress,,s.port@jeemayle.com,s.port@jeemayle.com,false,2025-04-01T00:00:00+01:00,,2025-04-01T00:00:00+01:00,single log,2025,,MHCLG,MHCLG,General needs,Affordable rent general needs local authority,No,2025-04-01,Affordable Rent,Affordable Rent,,,No,HIJKLMN,ABCDEFG,Yes,No,Affordable rent basis,Tenant abandoned property,No,,Address line 1,,London,,NW9 5LL,No,Barnet,E09000003,No,,,,,address line 1 as entered,address line 2 as entered,town or city as entered,county as entered,AB1 2CD,la as entered,House,Purpose built,Yes,3,2025-03-30,1,,Yes,2025-03-31,,,Don’t know,Yes,Assured Shorthold Tenancy (AST) – Fixed term,,2,4,,Yes,4,0,0,2,35,,Female,White,Irish,Australia,Other,Yes,Yes,,,32,Male,Not seeking work,No,Prefers not to say,Not known,Prefers not to say,Prefers not to say,Yes,Tenant prefers not to say,Not known,Person prefers not to say,Person prefers not to say,,,,,,,,,,,,,,,,,,,,,Yes – the person is a current or former regular,No – they left up to and including 5 years ago,Yes,No,Yes,Fully wheelchair accessible housing,Yes,No,No,No,No,No,No,Yes,No,No,Yes,No,No,No,No,No,No,No,Less than 1 year,1 year but under 2 years,Loss of tied accommodation,,,Other supported housing,No,Yes,TN23 6LZ,Yes,No,Ashford,E07000105,Yes,,Yes,,,,No,No,Yes,No,No,,,,Yes,No,Weekly,268,,Universal Credit housing element,Yes,All,,No,Every 2 weeks,200.0,100.0,,50.0,25.0,40.0,20.0,35.0,17.5,325.0,162.5,,,,Yes,Yes,12.0,6.0,,,,,,,,,,,,,,,,,,,, diff --git a/spec/fixtures/files/lettings_log_csv_export_non_support_codes_25.csv b/spec/fixtures/files/lettings_log_csv_export_non_support_codes_25.csv new file mode 100644 index 000000000..71588b16c --- /dev/null +++ b/spec/fixtures/files/lettings_log_csv_export_non_support_codes_25.csv @@ -0,0 +1,3 @@ +Log ID,Status of log,ID of a set of duplicate logs,User the log is assigned to,Is the user in the assigned_to column the data protection officer?,Time and date the log was created,User who last updated the log,Time and date the log was last updated,Was the log submitted in-service or via bulk upload?,Year collection period opened,ID of a set of bulk uploaded logs,Which organisation owns this property?,Which organisation manages this letting?,What is the needs type?,What is the letting type?,Is this letting a renewal?,What is the tenancy start date?,"What is the rent type? (grouped into SR, IR or AR)",What is the rent type?,Which type of Intermediate Rent is this letting?,Which 'Other' type of Intermediate Rent is this letting?,Is this a London Affordable Rent letting?,What is the tenant code?,What is the property reference?,Has the tenant seen the MHCLG privacy notice?,Is this the first time the property has been let as social housing?,What rent product was the property most recently let as?,What is the reason for the property being vacant?,Is this property new to the social rented sector?,Is the UPRN known?,"If known, property's UPRN",Address line 1,Address line 2,Town or City,County,Postcode,What is the property's local authority?,What type of unit is the property?,Which type of building is the property?,Is the property built or adapted to wheelchair-user standards?,How many bedrooms does the property have?,What is the void date?,Number of days the property was vacant,Were any major repairs carried out during the void period?,What date were any major repairs completed on?,Is this letting in sheltered accommodation?,Is this a joint tenancy?,Is this a starter tenancy?,What is the type of tenancy?,"If 'Other', what is the type of tenancy?",What is the length of the fixed-term tenancy to the nearest year?,How many people live in the household at this letting?,"Where household characteristics have a 'Refused' option for some or all of: AGE1-AGE8, SEX1-SEX8, RELAT2-RELAT8, ECSTAT1-ECSTAT8",What is the lead tenant's age?,Which of these best describes the lead tenant's gender identity?,What is the lead tenant's ethnic group?,Which of these best describes the lead tenant's ethnic background?,What is the lead tenant's nationality?,Which of these best describes the lead tenant's working situation?,What is person 2's relationship to the lead tenant?,What is person 2's age?,Which of these best describes person 2's gender identity?,Which of these best describes person 2's working situation?,What is person 3's relationship to the lead tenant?,What is person 3's age?,Which of these best describes person 3's gender identity?,Which of these best describes person 3's working situation?,What is person 4's relationship to the lead tenant?,What is person 4's age?,Which of these best describes person 4's gender identity?,Which of these best describes person 4's working situation?,What is person 5's relationship to the lead tenant?,What is person 5's age?,Which of these best describes person 5's gender identity?,Which of these best describes person 5's working situation?,What is person 6's relationship to the lead tenant?,What is person 6's age?,Which of these best describes person 6's gender identity?,Which of these best describes person 6's working situation?,What is person 7's relationship to the lead tenant?,What is person 7's age?,Which of these best describes person 7's gender identity?,Which of these best describes person 7's working situation?,What is person 8's relationship to the lead tenant?,What is person 8's age?,Which of these best describes person 8's gender identity?,Which of these best describes person 8's working situation?,Does anybody in the household have links to the UK armed forces?,Is this person still serving in the UK armed forces?,Was this person seriously injured or ill as a result of serving in the UK armed forces?,Is anybody in the household pregnant?,Does anybody in the household have any disabled access needs?,"What access needs do they have? (Fully wheelchair-accessible housing, Level access housing or Wheelchair access to essential rooms)",Disabled access needs a) Fully wheelchair-accessible housing,Disabled access needs b) Wheelchair access to essential rooms,Disabled access needs c) Level access housing,Disabled access needs f) Other disabled access needs,Disabled access needs g) No disabled access needs,Disabled access needs h) Don't know,Do they have any other disabled access needs?,Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more?,Does this person's condition affect their dexterity?,Does this person's condition affect their learning or understanding or concentrating?,Does this person's condition affect their hearing?,Does this person's condition affect their memory?,Does this person's condition affect their mental health?,Does this person's condition affect their mobility?,Does this person's condition affect them socially or behaviourally?,Does this person's condition affect their stamina or breathing or fatigue?,Does this person's condition affect their vision?,Does this person's condition affect them in another way?,How long has the household continuously lived in the local authority area of the new letting?,How long has the household been on the local authority waiting list for the new letting?,What is the tenant's main reason for the household leaving their last settled home?,"If 'Other', what was the main reason for leaving their last settled home?",Where was the household immediately before this letting?,Did the household experience homelessness immediately before this letting?,Previous postcode unknown or previous accommodation was temporary,What is the postcode of the household's last settled home?,Previous location LA name,Was the household given reasonable preference by the local authority?,Reasonable preference reason - They were homeless or about to lose their home (within 56 days),"Reasonable preference reason - They were living in insanitary, overcrowded or unisatisfactory housing",Reasonable preference reason - They needed to move on medical and welfare reasons (including disability),Reasonable preference reason - They needed to move to avoid hardship to themselves or others,Reasonable preference reason - Don't Know,Was the letting made under Choice-Based Lettings (CBL)?,Was the letting made under the Common Allocation Policy (CAP)?,Was the letting made under the Common Housing Register (CHR)?,Was the letting made under the Accessible Register?,"The letting was not allocated under CBL, CAP, CHR or Accessible Register.",,What was the source of referral for this letting?,Was the household income refused?,How often does the household receive income?,How much income does the household have in total?,Is the tenant likely to be receiving any of these housing-related benefits?,"Does the tenant receive housing-related benefits? Yes if hb = Universal Credit housing element or Housing benefit, No if hb = Don't Know, Neither, Tenant prefers not to say or blank","How much of the household's income is from Universal Credit, state pensions or benefits?",Does the household pay rent or other charges for the accommodation?,Does the household pay rent or other charges for the accommodation? - flag for when household_charge is answered no,How often does the household pay rent and other charges?,What is the basic rent?,What is the service charge?,What is the personal service charge?,What is the support charge?,Total charge to the tenant,"After the household has received any housing-related benefits, will they still need to pay for rent and charges?",Estimated outstanding amount,What scheme does this letting belong to?,"From scheme code, we map to the scheme name",Does the scheme contain confidential information?,"What is this type of scheme? (Direct access hostel), Foyer, Housing for older people or Other supported housing",Is this scheme registered under the Care Standards Act 2000?,Which organisation owns the housing stock for this scheme?,What client group is this scheme intended for?,Does this scheme provide for another client group?,What is the other client group?,What support does this scheme provide?,Intended length of stay,Date scheme was created,Which location is this letting for?,What is the postcode for this location?,What is the name of this location?,How many units are at this location?,What is the most common type of unit at this location?,What are the mobility standards for the majority of the units in this location?,What is the local authority of this postcode?,When did the first property in this location become available under this scheme? +id,status,duplicate_set_id,assigned_to,is_dpo,created_at,updated_by,updated_at,creation_method,collection_start_year,bulk_upload_id,owning_organisation_name,managing_organisation_name,needstype,lettype,renewal,startdate,renttype,renttype_detail,irproduct,irproduct_other,lar,tenancycode,propcode,declaration,first_time_property_let_as_social_housing,unitletas,rsnvac,newprop,uprn_known,uprn,address_line1,address_line2,town_or_city,county,postcode_full,la_label,unittype_gn,builtype,wchair,beds,voiddate,vacdays,majorrepairs,mrcdate,sheltered,joint,startertenancy,tenancy,tenancyother,tenancylength,hhmemb,refused,age1,sex1,ethnic_group,ethnic,nationality_all,ecstat1,relat2,age2,sex2,ecstat2,relat3,age3,sex3,ecstat3,relat4,age4,sex4,ecstat4,relat5,age5,sex5,ecstat5,relat6,age6,sex6,ecstat6,relat7,age7,sex7,ecstat7,relat8,age8,sex8,ecstat8,armedforces,leftreg,reservist,preg_occ,housingneeds,housingneeds_type,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_f,housingneeds_g,housingneeds_h,housingneeds_other,illness,illness_type_4,illness_type_5,illness_type_2,illness_type_6,illness_type_7,illness_type_3,illness_type_9,illness_type_8,illness_type_1,illness_type_10,layear,waityear,reason,reasonother,prevten,homeless,ppcodenk,ppostcode_full,prevloc_label,reasonpref,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,cbl,cap,chr,accessible_register,letting_allocation_none,referral_type,referral,incref,incfreq,earnings,hb,has_benefits,benefits,household_charge,nocharge,period,brent,scharge,pscharge,supcharg,tcharge,hbrentshortfall,tshortfall,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 +,in_progress,,choreographer@owtluk.com,false,2025-04-01T00:00:00+01:00,,2025-04-01T00:00:00+01:00,1,2025,,MHCLG,MHCLG,1,7,0,2025-04-01,2,2,,,2,HIJKLMN,ABCDEFG,1,0,2,6,2,0,,Address line 1,,London,,NW9 5LL,Barnet,7,1,1,3,2025-03-30,1,1,2025-03-31,,3,1,4,,2,4,1,35,F,0,2,36,0,P,32,M,6,R,-9,R,10,R,-9,R,10,,,,,,,,,,,,,,,,,1,4,1,2,1,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,2,7,4,,6,1,0,TN23 6LZ,Ashford,1,0,1,0,0,0,0,0,1,0,0,,,0,1,268,6,1,1,,0,2,200.0,50.0,40.0,35.0,325.0,1,12.0,,,,,,,,,,,,,,,,,,,, diff --git a/spec/fixtures/files/lettings_log_csv_export_non_support_labels_25.csv b/spec/fixtures/files/lettings_log_csv_export_non_support_labels_25.csv new file mode 100644 index 000000000..7244e286c --- /dev/null +++ b/spec/fixtures/files/lettings_log_csv_export_non_support_labels_25.csv @@ -0,0 +1,3 @@ +Log ID,Status of log,ID of a set of duplicate logs,User the log is assigned to,Is the user in the assigned_to column the data protection officer?,Time and date the log was created,User who last updated the log,Time and date the log was last updated,Was the log submitted in-service or via bulk upload?,Year collection period opened,ID of a set of bulk uploaded logs,Which organisation owns this property?,Which organisation manages this letting?,What is the needs type?,What is the letting type?,Is this letting a renewal?,What is the tenancy start date?,"What is the rent type? (grouped into SR, IR or AR)",What is the rent type?,Which type of Intermediate Rent is this letting?,Which 'Other' type of Intermediate Rent is this letting?,Is this a London Affordable Rent letting?,What is the tenant code?,What is the property reference?,Has the tenant seen the MHCLG privacy notice?,Is this the first time the property has been let as social housing?,What rent product was the property most recently let as?,What is the reason for the property being vacant?,Is this property new to the social rented sector?,Is the UPRN known?,"If known, property's UPRN",Address line 1,Address line 2,Town or City,County,Postcode,What is the property's local authority?,What type of unit is the property?,Which type of building is the property?,Is the property built or adapted to wheelchair-user standards?,How many bedrooms does the property have?,What is the void date?,Number of days the property was vacant,Were any major repairs carried out during the void period?,What date were any major repairs completed on?,Is this letting in sheltered accommodation?,Is this a joint tenancy?,Is this a starter tenancy?,What is the type of tenancy?,"If 'Other', what is the type of tenancy?",What is the length of the fixed-term tenancy to the nearest year?,How many people live in the household at this letting?,"Where household characteristics have a 'Refused' option for some or all of: AGE1-AGE8, SEX1-SEX8, RELAT2-RELAT8, ECSTAT1-ECSTAT8",What is the lead tenant's age?,Which of these best describes the lead tenant's gender identity?,What is the lead tenant's ethnic group?,Which of these best describes the lead tenant's ethnic background?,What is the lead tenant's nationality?,Which of these best describes the lead tenant's working situation?,What is person 2's relationship to the lead tenant?,What is person 2's age?,Which of these best describes person 2's gender identity?,Which of these best describes person 2's working situation?,What is person 3's relationship to the lead tenant?,What is person 3's age?,Which of these best describes person 3's gender identity?,Which of these best describes person 3's working situation?,What is person 4's relationship to the lead tenant?,What is person 4's age?,Which of these best describes person 4's gender identity?,Which of these best describes person 4's working situation?,What is person 5's relationship to the lead tenant?,What is person 5's age?,Which of these best describes person 5's gender identity?,Which of these best describes person 5's working situation?,What is person 6's relationship to the lead tenant?,What is person 6's age?,Which of these best describes person 6's gender identity?,Which of these best describes person 6's working situation?,What is person 7's relationship to the lead tenant?,What is person 7's age?,Which of these best describes person 7's gender identity?,Which of these best describes person 7's working situation?,What is person 8's relationship to the lead tenant?,What is person 8's age?,Which of these best describes person 8's gender identity?,Which of these best describes person 8's working situation?,Does anybody in the household have links to the UK armed forces?,Is this person still serving in the UK armed forces?,Was this person seriously injured or ill as a result of serving in the UK armed forces?,Is anybody in the household pregnant?,Does anybody in the household have any disabled access needs?,"What access needs do they have? (Fully wheelchair-accessible housing, Level access housing or Wheelchair access to essential rooms)",Disabled access needs a) Fully wheelchair-accessible housing,Disabled access needs b) Wheelchair access to essential rooms,Disabled access needs c) Level access housing,Disabled access needs f) Other disabled access needs,Disabled access needs g) No disabled access needs,Disabled access needs h) Don't know,Do they have any other disabled access needs?,Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more?,Does this person's condition affect their dexterity?,Does this person's condition affect their learning or understanding or concentrating?,Does this person's condition affect their hearing?,Does this person's condition affect their memory?,Does this person's condition affect their mental health?,Does this person's condition affect their mobility?,Does this person's condition affect them socially or behaviourally?,Does this person's condition affect their stamina or breathing or fatigue?,Does this person's condition affect their vision?,Does this person's condition affect them in another way?,How long has the household continuously lived in the local authority area of the new letting?,How long has the household been on the local authority waiting list for the new letting?,What is the tenant's main reason for the household leaving their last settled home?,"If 'Other', what was the main reason for leaving their last settled home?",Where was the household immediately before this letting?,Did the household experience homelessness immediately before this letting?,Previous postcode unknown or previous accommodation was temporary,What is the postcode of the household's last settled home?,Previous location LA name,Was the household given reasonable preference by the local authority?,Reasonable preference reason - They were homeless or about to lose their home (within 56 days),"Reasonable preference reason - They were living in insanitary, overcrowded or unisatisfactory housing",Reasonable preference reason - They needed to move on medical and welfare reasons (including disability),Reasonable preference reason - They needed to move to avoid hardship to themselves or others,Reasonable preference reason - Don't Know,Was the letting made under Choice-Based Lettings (CBL)?,Was the letting made under the Common Allocation Policy (CAP)?,Was the letting made under the Common Housing Register (CHR)?,Was the letting made under the Accessible Register?,"The letting was not allocated under CBL, CAP, CHR or Accessible Register.",,What was the source of referral for this letting?,Was the household income refused?,How often does the household receive income?,How much income does the household have in total?,Is the tenant likely to be receiving any of these housing-related benefits?,"Does the tenant receive housing-related benefits? Yes if hb = Universal Credit housing element or Housing benefit, No if hb = Don't Know, Neither, Tenant prefers not to say or blank","How much of the household's income is from Universal Credit, state pensions or benefits?",Does the household pay rent or other charges for the accommodation?,Does the household pay rent or other charges for the accommodation? - flag for when household_charge is answered no,How often does the household pay rent and other charges?,What is the basic rent?,What is the service charge?,What is the personal service charge?,What is the support charge?,Total charge to the tenant,"After the household has received any housing-related benefits, will they still need to pay for rent and charges?",Estimated outstanding amount,What scheme does this letting belong to?,"From scheme code, we map to the scheme name",Does the scheme contain confidential information?,"What is this type of scheme? (Direct access hostel), Foyer, Housing for older people or Other supported housing",Is this scheme registered under the Care Standards Act 2000?,Which organisation owns the housing stock for this scheme?,What client group is this scheme intended for?,Does this scheme provide for another client group?,What is the other client group?,What support does this scheme provide?,Intended length of stay,Date scheme was created,Which location is this letting for?,What is the postcode for this location?,What is the name of this location?,How many units are at this location?,What is the most common type of unit at this location?,What are the mobility standards for the majority of the units in this location?,What is the local authority of this postcode?,When did the first property in this location become available under this scheme? +id,status,duplicate_set_id,assigned_to,is_dpo,created_at,updated_by,updated_at,creation_method,collection_start_year,bulk_upload_id,owning_organisation_name,managing_organisation_name,needstype,lettype,renewal,startdate,renttype,renttype_detail,irproduct,irproduct_other,lar,tenancycode,propcode,declaration,first_time_property_let_as_social_housing,unitletas,rsnvac,newprop,uprn_known,uprn,address_line1,address_line2,town_or_city,county,postcode_full,la_label,unittype_gn,builtype,wchair,beds,voiddate,vacdays,majorrepairs,mrcdate,sheltered,joint,startertenancy,tenancy,tenancyother,tenancylength,hhmemb,refused,age1,sex1,ethnic_group,ethnic,nationality_all,ecstat1,relat2,age2,sex2,ecstat2,relat3,age3,sex3,ecstat3,relat4,age4,sex4,ecstat4,relat5,age5,sex5,ecstat5,relat6,age6,sex6,ecstat6,relat7,age7,sex7,ecstat7,relat8,age8,sex8,ecstat8,armedforces,leftreg,reservist,preg_occ,housingneeds,housingneeds_type,housingneeds_a,housingneeds_b,housingneeds_c,housingneeds_f,housingneeds_g,housingneeds_h,housingneeds_other,illness,illness_type_4,illness_type_5,illness_type_2,illness_type_6,illness_type_7,illness_type_3,illness_type_9,illness_type_8,illness_type_1,illness_type_10,layear,waityear,reason,reasonother,prevten,homeless,ppcodenk,ppostcode_full,prevloc_label,reasonpref,rp_homeless,rp_insan_unsat,rp_medwel,rp_hardship,rp_dontknow,cbl,cap,chr,accessible_register,letting_allocation_none,referral_type,referral,incref,incfreq,earnings,hb,has_benefits,benefits,household_charge,nocharge,period,brent,scharge,pscharge,supcharg,tcharge,hbrentshortfall,tshortfall,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 +,in_progress,,choreographer@owtluk.com,false,2025-04-01T00:00:00+01:00,,2025-04-01T00:00:00+01:00,single log,2025,,MHCLG,MHCLG,General needs,Affordable rent general needs local authority,No,2025-04-01,Affordable Rent,Affordable Rent,,,No,HIJKLMN,ABCDEFG,Yes,No,Affordable rent basis,Tenant abandoned property,No,No,,Address line 1,,London,,NW9 5LL,Barnet,House,Purpose built,Yes,3,2025-03-30,1,Yes,2025-03-31,,Don’t know,Yes,Assured Shorthold Tenancy (AST) – Fixed term,,2,4,Yes,35,Female,White,Irish,Australia,Other,Yes,32,Male,Not seeking work,Prefers not to say,Not known,Prefers not to say,Prefers not to say,Tenant prefers not to say,Not known,Person prefers not to say,Person prefers not to say,,,,,,,,,,,,,,,,,Yes – the person is a current or former regular,No – they left up to and including 5 years ago,Yes,No,Yes,Fully wheelchair accessible housing,Yes,No,No,No,No,No,No,Yes,No,No,Yes,No,No,No,No,No,No,No,Less than 1 year,1 year but under 2 years,Loss of tied accommodation,,Other supported housing,No,Yes,TN23 6LZ,Ashford,Yes,,Yes,,,,No,No,Yes,No,No,,,No,Weekly,268,Universal Credit housing element,Yes,All,,No,Every 2 weeks,200.0,50.0,40.0,35.0,325.0,Yes,12.0,,,,,,,,,,,,,,,,,,,, diff --git a/spec/helpers/tab_nav_helper_spec.rb b/spec/helpers/tab_nav_helper_spec.rb index bd29f4c46..71fd4f9a8 100644 --- a/spec/helpers/tab_nav_helper_spec.rb +++ b/spec/helpers/tab_nav_helper_spec.rb @@ -16,7 +16,7 @@ RSpec.describe TabNavHelper do describe "#org_cell" do it "returns the users org name and role separated by a newline character" do expected_html = "#{organisation.name}\nData provider" - expect(org_cell(current_user)).to match(expected_html) + expect(CGI.unescapeHTML(org_cell(current_user))).to match(expected_html) end end @@ -30,7 +30,7 @@ RSpec.describe TabNavHelper do describe "#scheme_cell" do it "returns the scheme link service name and primary user group separated by a newline character" do expected_html = "#{scheme.service_name}\nScheme" - expect(scheme_cell(scheme)).to match(expected_html) + expect(CGI.unescapeHTML(scheme_cell(scheme))).to match(expected_html) end end end diff --git a/spec/lib/tasks/update_manual_address_entry_selected_prexisting_logs_spec.rb b/spec/lib/tasks/update_manual_address_entry_selected_prexisting_logs_spec.rb new file mode 100644 index 000000000..cbcf6e0e8 --- /dev/null +++ b/spec/lib/tasks/update_manual_address_entry_selected_prexisting_logs_spec.rb @@ -0,0 +1,211 @@ +require "rails_helper" +require "rake" + +RSpec.describe "update_manual_address_entry_selected_preexisting_logs_spec", type: :task do + before do + Rake.application.rake_require("tasks/update_manual_address_entry_selected_prexisting_logs") + Rake::Task.define_task(:environment) + task.reenable + end + + describe "bulk_update:update_manual_address_entry_selected" do + let(:task) { Rake::Task["bulk_update:update_manual_address_entry_selected"] } + + let(:lettings_log_uprn_entered) do + build(:lettings_log, :completed, startdate: Time.zone.local(2024, 6, 1), needstype: 1, manual_address_entry_selected: false) + end + + let(:lettings_log_uprn_found) do + build(:lettings_log, :completed, startdate: Time.zone.local(2024, 9, 1), needstype: 1, manual_address_entry_selected: false, address_line1_input: "1 Test Street", postcode_full_input: "SW1 1AA") + end + + let(:lettings_log_address_fields_not_entered) do + build(:lettings_log, :inprogress_without_address_fields, startdate: Time.zone.local(2024, 9, 1), needstype: 1) + end + + let(:lettings_log_address_manually_entered) do + build(:lettings_log, :completed_without_uprn, startdate: Time.zone.local(2024, 12, 1), needstype: 1) + end + + let(:sales_log_uprn_entered) do + build(:sales_log, :completed, saledate: Time.zone.local(2024, 12, 1), manual_address_entry_selected: false) + end + + let(:sales_log_uprn_found) do + build(:sales_log, :completed, saledate: Time.zone.local(2024, 7, 1), manual_address_entry_selected: false, address_line1_input: "1 Test Street", postcode_full_input: "SW1 1AA") + end + + let(:sales_log_address_fields_not_entered) do + build(:sales_log, :inprogress_without_address_fields, saledate: Time.zone.local(2024, 12, 30)) + end + + let(:sales_log_address_manually_entered) do + build(:sales_log, :completed_without_uprn, saledate: Time.zone.local(2024, 12, 30)) + end + + context "when running the task" do + context "when logs do not meet the criteria" do + before do + lettings_log_uprn_found.save!(validate: false) + lettings_log_uprn_entered.save!(validate: false) + lettings_log_address_fields_not_entered.save!(validate: false) + + sales_log_uprn_found.save!(validate: false) + sales_log_uprn_entered.save!(validate: false) + sales_log_address_fields_not_entered.save!(validate: false) + end + + it "does not update logs with a UPRN entered" do + task.invoke + + lettings_log_uprn_entered.reload + sales_log_uprn_entered.reload + + expect(lettings_log_uprn_entered.manual_address_entry_selected).to be false + expect(lettings_log_uprn_entered.uprn).to eq("10033558653") + expect(sales_log_uprn_entered.manual_address_entry_selected).to be false + expect(sales_log_uprn_entered.uprn).to eq("10033558653") + end + + it "does not update logs with a UPRN found" do + task.invoke + + lettings_log_uprn_found.reload + sales_log_uprn_found.reload + + expect(lettings_log_uprn_found.manual_address_entry_selected).to be false + expect(lettings_log_uprn_found.uprn).to eq("10033558653") + expect(sales_log_uprn_found.manual_address_entry_selected).to be false + expect(sales_log_uprn_found.uprn).to eq("10033558653") + end + + it "does not update logs with no UPRN or address fields entered" do + task.invoke + + lettings_log_address_fields_not_entered.reload + sales_log_address_fields_not_entered.reload + + expect(lettings_log_address_fields_not_entered.manual_address_entry_selected).to be false + expect(sales_log_address_fields_not_entered.manual_address_entry_selected).to be false + end + end + + context "when logs do meet the criteria" do + before do + lettings_log_address_manually_entered.manual_address_entry_selected = false + lettings_log_address_manually_entered.save!(validate: false) + + sales_log_address_manually_entered.manual_address_entry_selected = false + sales_log_address_manually_entered.save!(validate: false) + end + + it "updates logs with an address manually entered" do + expect(lettings_log_address_manually_entered.manual_address_entry_selected).to be false + expect(lettings_log_address_manually_entered.address_line1).to eq("1 Test Street") + expect(lettings_log_address_manually_entered.address_line2).to eq("Testville") + expect(lettings_log_address_manually_entered.town_or_city).to eq("Testford") + expect(lettings_log_address_manually_entered.postcode_full).to eq("SW1 1AA") + + expect(sales_log_address_manually_entered.manual_address_entry_selected).to be false + expect(sales_log_address_manually_entered.address_line1).to eq("1 Test Street") + expect(sales_log_address_manually_entered.address_line2).to eq("Testville") + expect(sales_log_address_manually_entered.town_or_city).to eq("Testford") + expect(sales_log_address_manually_entered.postcode_full).to eq("SW1 1AA") + + task.invoke + + lettings_log_address_manually_entered.reload + sales_log_address_manually_entered.reload + + expect(lettings_log_address_manually_entered.manual_address_entry_selected).to be true + expect(lettings_log_address_manually_entered.address_line1).to eq("1 Test Street") + expect(lettings_log_address_manually_entered.address_line2).to eq("Testville") + expect(lettings_log_address_manually_entered.town_or_city).to eq("Testford") + expect(lettings_log_address_manually_entered.postcode_full).to eq("SW1 1AA") + + expect(sales_log_address_manually_entered.manual_address_entry_selected).to be true + expect(sales_log_address_manually_entered.address_line1).to eq("1 Test Street") + expect(sales_log_address_manually_entered.address_line2).to eq("Testville") + expect(sales_log_address_manually_entered.town_or_city).to eq("Testford") + expect(sales_log_address_manually_entered.postcode_full).to eq("SW1 1AA") + end + end + end + end + + describe "bulk_update:update_postcode_full_preexisting_manual_entry_logs" do + let(:task) { Rake::Task["bulk_update:update_postcode_full_preexisting_manual_entry_logs"] } + + let(:lettings_log_to_fix) do + build(:lettings_log, :inprogress_without_address_fields, startdate: Time.zone.local(2024, 6, 1), updated_at: Time.zone.parse("2025-03-19 16:30:00")) + end + + let(:bu_lettings_log_to_fix) do + build(:lettings_log, :inprogress_without_address_fields, startdate: Time.zone.local(2024, 6, 1), creation_method: "bulk upload", updated_at: Time.zone.parse("2025-03-19 16:30:00")) + end + + let(:lettings_log_not_to_fix) do + build(:lettings_log, :inprogress_without_address_fields, startdate: Time.zone.local(2024, 6, 1), updated_at: Time.zone.parse("2025-03-19 15:30:00")) + end + + before do + lettings_log_to_fix.manual_address_entry_selected = true + lettings_log_to_fix.address_line1 = "1 Test Street" + lettings_log_to_fix.address_line2 = "Testville" + lettings_log_to_fix.town_or_city = "Testford" + lettings_log_to_fix.postcode_full = nil + lettings_log_to_fix.address_line1_input = "1 Test Street" + lettings_log_to_fix.postcode_full_input = "SW1 2BB" + lettings_log_to_fix.save!(validate: false) + + bu_lettings_log_to_fix.manual_address_entry_selected = true + bu_lettings_log_to_fix.address_line1 = "1 Test Street" + bu_lettings_log_to_fix.address_line2 = "Testville" + bu_lettings_log_to_fix.town_or_city = "Testford" + bu_lettings_log_to_fix.postcode_full = nil + bu_lettings_log_to_fix.address_line1_as_entered = "1 Test Street" + bu_lettings_log_to_fix.postcode_full_as_entered = "SW1 2BB" + bu_lettings_log_to_fix.save!(validate: false) + + lettings_log_not_to_fix.postcode_full = nil + lettings_log_not_to_fix.save!(validate: false) + end + + context "when running the task" do + it "updates logs that meet the criteria" do + expect(lettings_log_to_fix.postcode_full).to be_nil + expect(lettings_log_to_fix.address_line1).to eq("1 Test Street") + expect(lettings_log_to_fix.address_line2).to eq("Testville") + expect(lettings_log_to_fix.town_or_city).to eq("Testford") + expect(lettings_log_to_fix.address_line1_input).to eq("1 Test Street") + expect(lettings_log_to_fix.postcode_full_input).to eq("SW1 2BB") + + expect(bu_lettings_log_to_fix.postcode_full).to be_nil + expect(bu_lettings_log_to_fix.address_line1_input).to be_nil + expect(bu_lettings_log_to_fix.address_line1).to eq("1 Test Street") + expect(bu_lettings_log_to_fix.address_line2).to eq("Testville") + expect(bu_lettings_log_to_fix.town_or_city).to eq("Testford") + expect(bu_lettings_log_to_fix.address_line1_as_entered).to eq("1 Test Street") + expect(bu_lettings_log_to_fix.postcode_full_as_entered).to eq("SW1 2BB") + + task.invoke + + lettings_log_to_fix.reload + bu_lettings_log_to_fix.reload + + expect(lettings_log_to_fix.postcode_full).to eq(lettings_log_to_fix.postcode_full_input) + expect(lettings_log_to_fix.postcode_full).to eq("SW1 2BB") + expect(bu_lettings_log_to_fix.postcode_full).to eq(bu_lettings_log_to_fix.postcode_full_as_entered) + expect(bu_lettings_log_to_fix.postcode_full).to eq("SW1 2BB") + end + + it "does not update logs that do not meet the criteria" do + task.invoke + + lettings_log_not_to_fix.reload + + expect(lettings_log_not_to_fix.postcode_full).to be_nil + end + end + end +end diff --git a/spec/models/form/lettings/questions/previous_let_type_spec.rb b/spec/models/form/lettings/questions/previous_let_type_spec.rb index 27ad8b0b0..ea8b5f67e 100644 --- a/spec/models/form/lettings/questions/previous_let_type_spec.rb +++ b/spec/models/form/lettings/questions/previous_let_type_spec.rb @@ -83,7 +83,7 @@ RSpec.describe Form::Lettings::Questions::PreviousLetType, type: :model do "6" => { "value" => "Rent to Buy basis" }, "7" => { "value" => "London Living Rent basis" }, "8" => { "value" => "Another Intermediate Rent basis" }, - "9" => { "value" => "Specified accommodation - exempt accommodation, manged properties, refuges and local authority hostels" }, + "9" => { "value" => "Specified accommodation - exempt accommodation, managed properties, refuges and local authority hostels" }, "divider" => { "value" => true }, "3" => { "value" => "Don’t know" }, }) diff --git a/spec/models/form/lettings/subsections/property_information_spec.rb b/spec/models/form/lettings/subsections/property_information_spec.rb index 2630c83d4..ed0add494 100644 --- a/spec/models/form/lettings/subsections/property_information_spec.rb +++ b/spec/models/form/lettings/subsections/property_information_spec.rb @@ -84,6 +84,14 @@ RSpec.describe Form::Lettings::Subsections::PropertyInformation, type: :model do ], ) end + + context "when it is supported housing and a renewal" do + let(:log) { FactoryBot.build(:lettings_log, needstype: 2, renewal: 1) } + + it "is not displayed in tasklist" do + expect(property_information.displayed_in_tasklist?(log)).to eq(false) + end + end end context "when 2025" do @@ -118,6 +126,14 @@ RSpec.describe Form::Lettings::Subsections::PropertyInformation, type: :model do ], ) end + + context "when it is supported housing and a renewal" do + let(:log) { FactoryBot.build(:lettings_log, needstype: 2, renewal: 1) } + + it "is displayed in tasklist" do + expect(property_information.displayed_in_tasklist?(log)).to eq(true) + end + end end end diff --git a/spec/models/form/sales/pages/previous_bedrooms_spec.rb b/spec/models/form/sales/pages/previous_bedrooms_spec.rb index 1899b03bc..a9f9f9337 100644 --- a/spec/models/form/sales/pages/previous_bedrooms_spec.rb +++ b/spec/models/form/sales/pages/previous_bedrooms_spec.rb @@ -31,6 +31,10 @@ RSpec.describe Form::Sales::Pages::PreviousBedrooms, type: :model do { "soctenant" => 0, }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 2 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 2 }, ]) end end diff --git a/spec/models/form/sales/pages/previous_property_type_spec.rb b/spec/models/form/sales/pages/previous_property_type_spec.rb index 357e7dc2e..41a988ee7 100644 --- a/spec/models/form/sales/pages/previous_property_type_spec.rb +++ b/spec/models/form/sales/pages/previous_property_type_spec.rb @@ -31,6 +31,10 @@ RSpec.describe Form::Sales::Pages::PreviousPropertyType, type: :model do { "soctenant" => 0, }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 2 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 2 }, ]) end end diff --git a/spec/models/form/sales/pages/previous_tenure_spec.rb b/spec/models/form/sales/pages/previous_tenure_spec.rb index 1dc5ec772..b5e8956cc 100644 --- a/spec/models/form/sales/pages/previous_tenure_spec.rb +++ b/spec/models/form/sales/pages/previous_tenure_spec.rb @@ -31,6 +31,10 @@ RSpec.describe Form::Sales::Pages::PreviousTenure, type: :model do { "soctenant" => 0, }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevten" => 2 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 1 }, + { "soctenant_is_inferred?" => true, "ownershipsch" => 1, "prevtenbuy2" => 2 }, ]) end end diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index e9493a017..9f0ddec42 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -849,6 +849,192 @@ RSpec.describe LettingsLog do expect(lettings_log.reload.is_la_inferred).to eq(false) end end + + context "when the log changes from new build to not new build" do + before do + allow(FormHandler.instance).to receive(:current_lettings_form).and_call_original + Timecop.freeze(2025, 5, 1) + Singleton.__init__(FormHandler) + end + + after do + Timecop.unfreeze + end + + context "and the address is entered" do + let(:address_lettings_log) do + create(:lettings_log, + :setup_completed, + startdate: Time.zone.yesterday, + mrcdate: nil, + rsnvac: 15, + manual_address_entry_selected: true, + first_time_property_let_as_social_housing: 1, + address_line1: "Address line 1", + address_line2: "Address line 2", + town_or_city: "Town", + postcode_full: "AA1 1AA") + end + + it "keeps the manually entered address" do + expect(address_lettings_log.manual_address_entry_selected).to eq(true) + expect(address_lettings_log.uprn_selection).to eq(nil) + expect(address_lettings_log.uprn_known).to eq(0) + expect(address_lettings_log.uprn).to eq(nil) + expect(address_lettings_log.address_line1).to eq("Address line 1") + expect(address_lettings_log.address_line2).to eq("Address line 2") + expect(address_lettings_log.town_or_city).to eq("Town") + expect(address_lettings_log.postcode_full).to eq("AA1 1AA") + + address_lettings_log.update!(rsnvac: 16) + expect(address_lettings_log.manual_address_entry_selected).to eq(true) + expect(address_lettings_log.address_line1).to eq("Address line 1") + expect(address_lettings_log.address_line2).to eq("Address line 2") + expect(address_lettings_log.town_or_city).to eq("Town") + expect(address_lettings_log.postcode_full).to eq("AA1 1AA") + end + end + + context "and the address is not entered" do + let(:address_lettings_log) do + create(:lettings_log, + :setup_completed, + startdate: Time.zone.yesterday, + mrcdate: nil, + rsnvac: 15, + manual_address_entry_selected: true, + first_time_property_let_as_social_housing: 1, + address_line1: nil, + address_line2: nil, + town_or_city: nil, + postcode_full: nil) + end + + it "routes to the uprn question" do + expect(address_lettings_log.manual_address_entry_selected).to eq(true) + expect(address_lettings_log.uprn_selection).to eq(nil) + expect(address_lettings_log.uprn_known).to eq(0) + expect(address_lettings_log.uprn).to eq(nil) + expect(address_lettings_log.address_line1).to eq(nil) + expect(address_lettings_log.address_line2).to eq(nil) + expect(address_lettings_log.town_or_city).to eq(nil) + expect(address_lettings_log.postcode_full).to eq(nil) + + address_lettings_log.update!(rsnvac: 16) + expect(address_lettings_log.manual_address_entry_selected).to eq(false) + expect(address_lettings_log.uprn_selection).to eq(nil) + expect(address_lettings_log.uprn_known).to eq(nil) + end + end + end + + context "when the log changes from not new build to new build" do + before do + allow(FormHandler.instance).to receive(:current_lettings_form).and_call_original + Timecop.freeze(2025, 5, 1) + Singleton.__init__(FormHandler) + end + + after do + Timecop.unfreeze + end + + context "and the uprn is selected" do + let(:address_lettings_log) do + create(:lettings_log, + :setup_completed, + startdate: Time.zone.yesterday, + mrcdate: nil, + rsnvac: 17, + manual_address_entry_selected: false, + first_time_property_let_as_social_housing: 1, + uprn_selection: "1", + uprn_confirmed: "1", + uprn_known: "1", + uprn: "1") + end + + it "keeps the uprn" do + expect(address_lettings_log.manual_address_entry_selected).to eq(false) + expect(address_lettings_log.uprn).to eq("1") + expect(address_lettings_log.address_line1).to eq("1, Test Street") + expect(address_lettings_log.town_or_city).to eq("Test Town") + expect(address_lettings_log.postcode_full).to eq("AA1 1AA") + + address_lettings_log.update!(rsnvac: 15) + expect(address_lettings_log.manual_address_entry_selected).to eq(false) + expect(address_lettings_log.address_line1).to eq("1, Test Street") + expect(address_lettings_log.town_or_city).to eq("Test Town") + expect(address_lettings_log.postcode_full).to eq("AA1 1AA") + expect(address_lettings_log.uprn_selection).to eq("1") + expect(address_lettings_log.uprn).to eq("1") + expect(address_lettings_log.uprn_known).to eq(1) + end + end + + context "and the address is manually entered" do + let(:address_lettings_log) do + create(:lettings_log, + :setup_completed, + startdate: Time.zone.yesterday, + mrcdate: nil, + rsnvac: 16, + manual_address_entry_selected: true, + first_time_property_let_as_social_housing: 1, + uprn_selection: "uprn_not_listed", + address_line1: "Address line 1", + address_line2: "Address line 2", + town_or_city: "Town", + postcode_full: "AA1 1AA") + end + + it "keeps the manually entered address" do + expect(address_lettings_log.manual_address_entry_selected).to eq(true) + expect(address_lettings_log.address_line1).to eq("Address line 1") + expect(address_lettings_log.address_line2).to eq("Address line 2") + expect(address_lettings_log.town_or_city).to eq("Town") + expect(address_lettings_log.postcode_full).to eq("AA1 1AA") + + address_lettings_log.update!(rsnvac: 15) + expect(address_lettings_log.manual_address_entry_selected).to eq(true) + expect(address_lettings_log.address_line1).to eq("Address line 1") + expect(address_lettings_log.address_line2).to eq("Address line 2") + expect(address_lettings_log.town_or_city).to eq("Town") + expect(address_lettings_log.postcode_full).to eq("AA1 1AA") + end + end + + context "and the address is not entered" do + let(:address_lettings_log) do + create(:lettings_log, + :setup_completed, + startdate: Time.zone.yesterday, + mrcdate: nil, + rsnvac: 17, + manual_address_entry_selected: false, + first_time_property_let_as_social_housing: 1, + address_line1: nil, + address_line2: nil, + town_or_city: nil, + postcode_full: nil) + end + + it "routes to the manual address questions" do + expect(address_lettings_log.manual_address_entry_selected).to eq(false) + expect(address_lettings_log.uprn_selection).to eq(nil) + expect(address_lettings_log.address_line1).to eq(nil) + expect(address_lettings_log.address_line2).to eq(nil) + expect(address_lettings_log.town_or_city).to eq(nil) + expect(address_lettings_log.postcode_full).to eq(nil) + + address_lettings_log.update!(rsnvac: 15) + expect(address_lettings_log.manual_address_entry_selected).to eq(true) + expect(address_lettings_log.uprn_selection).to eq(nil) + expect(address_lettings_log.uprn).to eq(nil) + expect(address_lettings_log.uprn_known).to eq(0) + end + end + end end describe "optional fields" do diff --git a/spec/models/validations/soft_validations_spec.rb b/spec/models/validations/soft_validations_spec.rb index b4188ba8c..0027c2d71 100644 --- a/spec/models/validations/soft_validations_spec.rb +++ b/spec/models/validations/soft_validations_spec.rb @@ -265,6 +265,24 @@ RSpec.describe Validations::SoftValidations do expect(record.major_repairs_date_in_soft_range?).to be false end end + + context "with 2025 logs" do + context "when the void date is within 20 years of the tenancy start date" do + it "shows the interruption screen" do + record.startdate = Time.zone.local(2026, 2, 1) + record.mrcdate = Time.zone.local(2007, 2, 1) + expect(record.major_repairs_date_in_soft_range?).to be true + end + end + + context "when the void date is less than 2 years before the tenancy start date" do + it "does not show the interruption screen" do + record.startdate = Time.zone.local(2026, 2, 1) + record.mrcdate = Time.zone.local(2025, 2, 1) + expect(record.major_repairs_date_in_soft_range?).to be false + end + end + end end describe "void date soft validations" do @@ -283,6 +301,24 @@ RSpec.describe Validations::SoftValidations do expect(record.voiddate_in_soft_range?).to be false end end + + context "with 2025 logs" do + context "when the void date is within 20 years of the tenancy start date" do + it "shows the interruption screen" do + record.startdate = Time.zone.local(2026, 2, 1) + record.voiddate = Time.zone.local(2007, 2, 1) + expect(record.voiddate_in_soft_range?).to be true + end + end + + context "when the void date is less than 2 years before the tenancy start date" do + it "does not show the interruption screen" do + record.startdate = Time.zone.local(2026, 2, 1) + record.voiddate = Time.zone.local(2025, 2, 1) + expect(record.voiddate_in_soft_range?).to be false + end + end + end end describe "old persons shared ownership soft validations" do diff --git a/spec/requests/address_search_controller_spec.rb b/spec/requests/address_search_controller_spec.rb index 5c2acd11a..1dffdf8cf 100644 --- a/spec/requests/address_search_controller_spec.rb +++ b/spec/requests/address_search_controller_spec.rb @@ -25,7 +25,7 @@ RSpec.describe AddressSearchController, type: :request do expect(sales_log.town_or_city).to eq(nil) expect(sales_log.la).to eq(nil) - get "/address-search/manual-input/sales_log/#{sales_log.id}" + get address_manual_input_path(log_type: "sales_log", log_id: sales_log.id) sales_log.reload expect(sales_log.manual_address_entry_selected).to eq(true) @@ -58,7 +58,7 @@ RSpec.describe AddressSearchController, type: :request do expect(lettings_log.town_or_city).to eq("London") expect(lettings_log.la).to eq("E09000033") - get "/address-search/manual-input/lettings_log/#{lettings_log.id}" + get address_manual_input_path(log_type: "lettings_log", log_id: lettings_log.id) lettings_log.reload expect(lettings_log.manual_address_entry_selected).to eq(true) @@ -94,7 +94,7 @@ RSpec.describe AddressSearchController, type: :request do expect(lettings_log.town_or_city).to eq(nil) expect(lettings_log.la).to eq(nil) - get "/address-search/search-input/lettings_log/#{lettings_log.id}" + get address_search_input_path(log_type: "lettings_log", log_id: lettings_log.id) lettings_log.reload expect(lettings_log.manual_address_entry_selected).to eq(false) @@ -128,7 +128,7 @@ RSpec.describe AddressSearchController, type: :request do expect(sales_log.town_or_city).to eq("Test Town") expect(sales_log.la).to eq("E09000033") - get "/address-search/search-input/sales_log/#{sales_log.id}" + get address_search_input_path(log_type: "sales_log", log_id: sales_log.id) sales_log.reload expect(sales_log.manual_address_entry_selected).to eq(false) @@ -144,5 +144,109 @@ RSpec.describe AddressSearchController, type: :request do expect(sales_log.la).to eq(nil) end end + + context "when searching by address and UPRN" do + let(:sales_log) { create(:sales_log, :completed, manual_address_entry_selected: false, assigned_to: user) } + + context "and theres no uprn returned" do + before do + body = { results: [{ DPA: { "ADDRESS": "100, Test Street", "UPRN": "100" } }] }.to_json + uprn_body = { results: [{ DPA: nil }] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?key=OS_DATA_KEY&maxresults=10&minmatch=0.2&query=100") + .to_return(status: 200, body:, headers: {}) + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=100") + .to_return(status: 200, body: uprn_body, headers: {}) + end + + it "returns the address results" do + get address_search_path, params: { query: "100" } + + expect(response).to have_http_status(:ok) + expect(response.body).to eq([{ text: "100, Test Street", value: "100" }].to_json) + end + end + + context "and theres no address returned" do + before do + body = { results: [{ DPA: nil }] }.to_json + uprn_body = { results: [{ DPA: { "ADDRESS": "321, Test Street", UPRN: "321" } }] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?key=OS_DATA_KEY&maxresults=10&minmatch=0.2&query=321") + .to_return(status: 200, body:, headers: {}) + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=321") + .to_return(status: 200, body: uprn_body, headers: {}) + end + + it "returns the address results" do + get address_search_path, params: { query: "321" } + + expect(response).to have_http_status(:ok) + expect(response.body).to eq([{ text: "321, Test Street", value: "321" }].to_json) + end + end + end + end + + describe "GET #index" do + context "when query is nil" do + it "returns a bad request error" do + get address_search_path, params: { query: nil } + expect(response).to have_http_status(:bad_request) + expect(response.body).to include("Query cannot be blank.") + end + end + + context "when query is all numbers and greater than 5 digits" do + before do + address_body = { results: [{ DPA: { "ADDRESS": "Path not taken", UPRN: "111" } }] }.to_json + uprn_body = { results: [{ DPA: { "ADDRESS": "2, Test Street", UPRN: "123456" } }] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?key=OS_DATA_KEY&maxresults=10&minmatch=0.2&query=123456") + .to_return(status: 200, body: address_body, headers: {}) + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=123456") + .to_return(status: 200, body: uprn_body, headers: {}) + end + + it "assumes it's a UPRN and returns the address" do + get address_search_path, params: { query: "123456" } + expect(response).to have_http_status(:ok) + expect(response.body).to include("2, Test Street") + expect(response.body).not_to include("Path not taken") + end + end + + context "when query contains any non-digit characters" do + before do + address_body = { results: [{ DPA: { "ADDRESS": "70, Test Street", UPRN: "123777" } }] }.to_json + uprn_body = { results: [{ DPA: { "ADDRESS": "Path not taken", UPRN: "111" } }] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?key=OS_DATA_KEY&maxresults=10&minmatch=0.2&query=70,") + .to_return(status: 200, body: address_body, headers: {}) + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=70,") + .to_return(status: 200, body: uprn_body, headers: {}) + end + + it "assumes it's an address and returns the address results" do + get address_search_path, params: { query: "70," } + expect(response).to have_http_status(:ok) + expect(response.body).to include("70, Test Street") + expect(response.body).not_to include("Path not taken") + end + end + + context "when query is ambiguous" do + before do + address_body = { results: [{ DPA: { "ADDRESS": "111, Test Street", UPRN: "123777" } }] }.to_json + uprn_body = { results: [{ DPA: { "ADDRESS": "70 Bean Road", UPRN: "111" } }] }.to_json + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/find?key=OS_DATA_KEY&maxresults=10&minmatch=0.2&query=111") + .to_return(status: 200, body: address_body, headers: {}) + WebMock.stub_request(:get, "https://api.os.uk/search/places/v1/uprn?dataset=DPA,LPI&key=OS_DATA_KEY&uprn=111") + .to_return(status: 200, body: uprn_body, headers: {}) + end + + it "uses both APIs and merges results" do + get address_search_path, params: { query: "111" } + expect(response).to have_http_status(:ok) + expect(response.body).to include("111, Test Street") + expect(response.body).to include("70 Bean Road") + end + end end end diff --git a/spec/requests/bulk_upload_lettings_results_controller_spec.rb b/spec/requests/bulk_upload_lettings_results_controller_spec.rb index 3c84487ac..3416b6da9 100644 --- a/spec/requests/bulk_upload_lettings_results_controller_spec.rb +++ b/spec/requests/bulk_upload_lettings_results_controller_spec.rb @@ -138,7 +138,7 @@ RSpec.describe BulkUploadLettingsResultsController, type: :request do get "/lettings-logs/bulk-upload-results/#{bulk_upload.id}/summary" expect(response.body).to include("This error report is out of date.") - expect(response.body).to include("Some logs in this upload are assigned to #{other_user.name}, who has moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.") + expect(CGI.unescapeHTML(response.body)).to include("Some logs in this upload are assigned to #{other_user.name}, who has moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.") end end diff --git a/spec/requests/bulk_upload_sales_results_controller_spec.rb b/spec/requests/bulk_upload_sales_results_controller_spec.rb index e58303d68..2236475fa 100644 --- a/spec/requests/bulk_upload_sales_results_controller_spec.rb +++ b/spec/requests/bulk_upload_sales_results_controller_spec.rb @@ -30,7 +30,7 @@ RSpec.describe BulkUploadSalesResultsController, type: :request do get "/sales-logs/bulk-upload-results/#{bulk_upload.id}/summary" expect(response.body).to include("This error report is out of date.") - expect(response.body).to include("Some logs in this upload are assigned to #{user.name}, who has moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.") + expect(CGI.unescapeHTML(response.body)).to include("Some logs in this upload are assigned to #{user.name}, who has moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.") end end @@ -113,7 +113,7 @@ RSpec.describe BulkUploadSalesResultsController, type: :request do get "/sales-logs/bulk-upload-results/#{bulk_upload.id}/summary" expect(response.body).to include("This error report is out of date.") - expect(response.body).to include("Some logs in this upload are assigned to #{other_user.name}, who has moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.") + expect(CGI.unescapeHTML(response.body)).to include("Some logs in this upload are assigned to #{other_user.name}, who has moved to a different organisation since this file was uploaded. Upload the file again to get an accurate error report.") end end diff --git a/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb index 28aac4fea..0074b3712 100644 --- a/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb @@ -60,7 +60,7 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do field_31: "28", field_32: "1", field_33: "1", - field_34: "R", + field_34: "3", field_35: "32", field_36: "F", field_37: "17", @@ -1145,6 +1145,52 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do end end + describe "relationship field mappings" do + [ + %w[field_34 relat2 2], + %w[field_42 relat3 3], + %w[field_46 relat4 4], + %w[field_50 relat5 5], + %w[field_54 relat6 6], + ].each do |input_field, relationship_attribute, person_num| + describe input_field.to_s do + context "when #{input_field} is 1" do + let(:attributes) { setup_section_params.merge({ input_field.to_sym => "1", field_41: "5" }) } + + it "sets relationship to P" do + expect(parser.log.public_send(relationship_attribute)).to eq("P") + end + end + + context "when #{input_field} is 2" do + let(:attributes) { setup_section_params.merge({ input_field.to_sym => "2", field_41: "5" }) } + + it "sets relationship to X" do + expect(parser.log.public_send(relationship_attribute)).to eq("X") + end + end + + context "when #{input_field} is 3" do + let(:attributes) { setup_section_params.merge({ input_field.to_sym => "3", field_41: "5" }) } + + it "sets relationship to R" do + expect(parser.log.public_send(relationship_attribute)).to eq("R") + end + end + + context "when #{input_field} is 4" do + let(:attributes) { setup_section_params.merge({ input_field.to_sym => "4", field_41: "5" }) } + + it "gives a validation error" do + parser.valid? + validation_message = "You must answer person #{person_num} is the partner of buyer 1." + expect(parser.errors[input_field]).to include validation_message + end + end + end + end + end + describe "field_39" do # ecstat2 context "when buyer 2 has no age but has ecstat as child" do let(:attributes) { valid_attributes.merge({ field_35: nil, field_39: "9" }) } diff --git a/spec/services/csv/lettings_log_csv_service_spec.rb b/spec/services/csv/lettings_log_csv_service_spec.rb index 7a0e15a12..bf45bbe30 100644 --- a/spec/services/csv/lettings_log_csv_service_spec.rb +++ b/spec/services/csv/lettings_log_csv_service_spec.rb @@ -194,6 +194,199 @@ RSpec.describe Csv::LettingsLogCsvService do end describe "the full CSV output" do + context "when the requested log year is 2025" do + let(:year) { 2025 } + let(:organisation) { create(:organisation, provider_type: "LA", name: "MHCLG") } + let(:log) do + create( + :lettings_log, + :ignore_validation_errors, + created_by: user, + assigned_to: user, + created_at: Time.zone.local(2025, 4, 1), + updated_at: Time.zone.local(2025, 4, 1), + owning_organisation: organisation, + managing_organisation: organisation, + needstype: 1, + renewal: 0, + startdate: Time.zone.local(2025, 4, 1), + rent_type: 1, + tenancycode: "HIJKLMN", + propcode: "ABCDEFG", + declaration: 1, + address_line1: "Address line 1", + town_or_city: "London", + postcode_full: "NW9 5LL", + la: "E09000003", + is_la_inferred: false, + address_line1_as_entered: "address line 1 as entered", + address_line2_as_entered: "address line 2 as entered", + town_or_city_as_entered: "town or city as entered", + county_as_entered: "county as entered", + postcode_full_as_entered: "AB1 2CD", + la_as_entered: "la as entered", + first_time_property_let_as_social_housing: 0, + unitletas: 2, + rsnvac: 6, + unittype_gn: 7, + builtype: 1, + wchair: 1, + beds: 3, + voiddate: Time.zone.local(2025, 3, 30), + majorrepairs: 1, + mrcdate: Time.zone.local(2025, 3, 31), + joint: 3, + startertenancy: 1, + tenancy: 4, + tenancylength: 2, + hhmemb: 4, + age1_known: 0, + age1: 35, + sex1: "F", + ethnic_group: 0, + ethnic: 2, + nationality_all: 36, + ecstat1: 0, + details_known_2: 0, + relat2: "P", + age2_known: 0, + age2: 32, + sex2: "M", + ecstat2: 6, + details_known_3: 1, + details_known_4: 0, + relat4: "R", + age4_known: 1, + sex4: "R", + ecstat4: 10, + armedforces: 1, + leftreg: 4, + reservist: 1, + preg_occ: 2, + housingneeds: 1, + housingneeds_type: 0, + housingneeds_a: 1, + housingneeds_b: 0, + housingneeds_c: 0, + housingneeds_f: 0, + housingneeds_g: 0, + housingneeds_h: 0, + housingneeds_other: 0, + illness: 1, + illness_type_1: 0, + illness_type_2: 1, + illness_type_3: 0, + illness_type_4: 0, + illness_type_5: 0, + illness_type_6: 0, + illness_type_7: 0, + illness_type_8: 0, + illness_type_9: 0, + illness_type_10: 0, + layear: 2, + waityear: 7, + reason: 4, + prevten: 6, + homeless: 1, + ppcodenk: 1, + ppostcode_full: "TN23 6LZ", + previous_la_known: 1, + prevloc: "E07000105", + reasonpref: 1, + rp_homeless: 0, + rp_insan_unsat: 1, + rp_medwel: 0, + rp_hardship: 0, + rp_dontknow: 0, + cbl: 0, + chr: 1, + cap: 0, + accessible_register: 0, + referral: 2, + net_income_known: 0, + incref: 0, + incfreq: 1, + earnings: 268, + hb: 6, + has_benefits: 1, + benefits: 1, + period: 2, + brent: 200, + scharge: 50, + pscharge: 40, + supcharg: 35, + tcharge: 325, + hbrentshortfall: 1, + tshortfall_known: 1, + tshortfall: 12, + ) + end + + context "when exporting with human readable labels" do + let(:export_type) { "labels" } + + context "when the current user is a support user" do + let(:user) { create(:user, :support, organisation:, email: "s.port@jeemayle.com") } + + it "exports the CSV with 2025 ordering and all values correct" do + expected_content = CSV.read("spec/fixtures/files/lettings_log_csv_export_labels_25.csv") + values_to_delete = %w[id] + values_to_delete.each do |attribute| + index = attribute_line.index(attribute) + content_line[index] = nil + end + expect(csv).to eq expected_content + end + end + + context "when the current user is not a support user" do + let(:user) { create(:user, :data_provider, organisation:, email: "choreographer@owtluk.com") } + + it "exports the CSV with all values correct" do + expected_content = CSV.read("spec/fixtures/files/lettings_log_csv_export_non_support_labels_25.csv") + values_to_delete = %w[id] + values_to_delete.each do |attribute| + index = attribute_line.index(attribute) + content_line[index] = nil + end + expect(csv).to eq expected_content + end + end + end + + context "when exporting values as codes" do + let(:export_type) { "codes" } + + context "when the current user is a support user" do + let(:user) { create(:user, :support, organisation:, email: "s.port@jeemayle.com") } + + it "exports the CSV with all values correct" do + expected_content = CSV.read("spec/fixtures/files/lettings_log_csv_export_codes_25.csv") + values_to_delete = %w[id] + values_to_delete.each do |attribute| + index = attribute_line.index(attribute) + content_line[index] = nil + end + expect(csv).to eq expected_content + end + end + + context "when the current user is not a support user" do + let(:user) { create(:user, :data_provider, organisation:, email: "choreographer@owtluk.com") } + + it "exports the CSV with all values correct" do + expected_content = CSV.read("spec/fixtures/files/lettings_log_csv_export_non_support_codes_25.csv") + values_to_delete = %w[id] + values_to_delete.each do |attribute| + index = attribute_line.index(attribute) + content_line[index] = nil + end + expect(csv).to eq expected_content + end + end + end + end + context "when the requested log year is 2024" do let(:year) { 2024 } let(:organisation) { create(:organisation, provider_type: "LA", name: "MHCLG") } diff --git a/spec/services/exports/export_service_spec.rb b/spec/services/exports/export_service_spec.rb index fb52c5274..051a0f38d 100644 --- a/spec/services/exports/export_service_spec.rb +++ b/spec/services/exports/export_service_spec.rb @@ -9,12 +9,14 @@ RSpec.describe Exports::ExportService do let(:user) { FactoryBot.create(:user, email: "test1@example.com") } let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: {}) } let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: {}) } + let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: {}) } before do Timecop.freeze(start_time) Singleton.__init__(FormHandler) allow(storage_service).to receive(:write_file) allow(Exports::LettingsLogExportService).to receive(:new).and_return(lettings_logs_export_service) + allow(Exports::SalesLogExportService).to receive(:new).and_return(sales_logs_export_service) allow(Exports::UserExportService).to receive(:new).and_return(users_export_service) allow(Exports::OrganisationExportService).to receive(:new).and_return(organisations_export_service) end @@ -23,7 +25,9 @@ RSpec.describe Exports::ExportService do Timecop.return end - context "when exporting daily XMLs" do + context "when exporting daily XMLs before 2025" do + let(:start_time) { Time.zone.local(2022, 5, 1) } + context "and no lettings archives get created in lettings logs export" do let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: {}) } @@ -193,6 +197,64 @@ RSpec.describe Exports::ExportService do end end end + + context "and multiple sales archives get created in sales logs export" do + let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time, "second_file_base_name" => start_time }) } + let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: { "some_sales_file_base_name" => start_time, "second_sales_file_base_name" => start_time }) } + + context "and no user archives get created in user export" do + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers and correct data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2022-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2022-05-01 00:00:00 +0100,second_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + + context "and multiple user archive gets created in user export" do + let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time, "second_user_file_base_name" => start_time }) } + + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers and correct data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2022-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2022-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_user_file_base_name,2022-05-01 00:00:00 +0100,some_user_file_base_name.zip\nsecond_user_file_base_name,2022-05-01 00:00:00 +0100,second_user_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + + context "and multiple user and organisation archives gets created in user export" do + let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time, "second_user_file_base_name" => start_time }) } + let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: { "some_organisation_file_base_name" => start_time, "second_organisation_file_base_name" => start_time }) } + + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers and correct data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2022-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2022-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_user_file_base_name,2022-05-01 00:00:00 +0100,some_user_file_base_name.zip\nsecond_user_file_base_name,2022-05-01 00:00:00 +0100,second_user_file_base_name.zip\nsome_organisation_file_base_name,2022-05-01 00:00:00 +0100,some_organisation_file_base_name.zip\nsecond_organisation_file_base_name,2022-05-01 00:00:00 +0100,second_organisation_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + end end context "when exporting specific lettings log collection" do @@ -204,7 +266,7 @@ RSpec.describe Exports::ExportService do it "generates a master manifest with the correct name" do expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) - export_service.export_xml(full_update: true, collection: "2022") + export_service.export_xml(full_update: true, collection: "lettings", year: "2022") end it "does not write user data" do @@ -212,7 +274,7 @@ RSpec.describe Exports::ExportService do expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\n" allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } - export_service.export_xml(full_update: true, collection: "2022") + export_service.export_xml(full_update: true, collection: "lettings", year: "2022") expect(actual_content).to eq(expected_content) end end @@ -226,7 +288,7 @@ RSpec.describe Exports::ExportService do it "generates a master manifest with the correct name" do expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) - export_service.export_xml(full_update: true, collection: "2023") + export_service.export_xml(full_update: true, collection: "lettings", year: "2023") end it "does not write user data" do @@ -234,7 +296,7 @@ RSpec.describe Exports::ExportService do expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2022-05-01 00:00:00 +0100,some_file_base_name.zip\n" allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } - export_service.export_xml(full_update: true, collection: "2023") + export_service.export_xml(full_update: true, collection: "lettings", year: "2023") expect(actual_content).to eq(expected_content) end end @@ -330,4 +392,179 @@ RSpec.describe Exports::ExportService do end end end + + context "with date after 2025-04-01" do + let(:start_time) { Time.zone.local(2025, 5, 1) } + let(:expected_master_manifest_filename) { "Manifest_2025_05_01_0001.csv" } + + context "when exporting daily XMLs" do + context "and no sales archives get created in sales logs export" do + let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: {}) } + let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: {}) } + + context "and no user or organisation archives get created in user export" do + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers but no data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + end + + context "and one sales archive gets created in sales logs export" do + let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: { "some_sales_file_base_name" => start_time }) } + let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time }) } + + context "and no user archives get created in user export" do + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers and correct data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + + context "and one user archive gets created in user export" do + let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time }) } + + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers and correct data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\nsome_user_file_base_name,2025-05-01 00:00:00 +0100,some_user_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + end + + context "and multiple sales archives get created in sales logs export" do + let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: { "some_sales_file_base_name" => start_time, "second_sales_file_base_name" => start_time }) } + let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: { "some_file_base_name" => start_time, "second_file_base_name" => start_time }) } + + context "and no user archives get created in user export" do + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers and correct data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2025-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\nsecond_sales_file_base_name,2025-05-01 00:00:00 +0100,second_sales_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + + context "and multiple user archive gets created in user export" do + let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time, "second_user_file_base_name" => start_time }) } + + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers and correct data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2025-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\nsecond_sales_file_base_name,2025-05-01 00:00:00 +0100,second_sales_file_base_name.zip\nsome_user_file_base_name,2025-05-01 00:00:00 +0100,some_user_file_base_name.zip\nsecond_user_file_base_name,2025-05-01 00:00:00 +0100,second_user_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + + context "and multiple user and organisation archives gets created in user export" do + let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time, "second_user_file_base_name" => start_time }) } + let(:organisations_export_service) { instance_double("Exports::OrganisationExportService", export_xml_organisations: { "some_organisation_file_base_name" => start_time, "second_organisation_file_base_name" => start_time }) } + + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml + end + + it "generates a master manifest with CSV headers and correct data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_file_base_name,2025-05-01 00:00:00 +0100,some_file_base_name.zip\nsecond_file_base_name,2025-05-01 00:00:00 +0100,second_file_base_name.zip\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\nsecond_sales_file_base_name,2025-05-01 00:00:00 +0100,second_sales_file_base_name.zip\nsome_user_file_base_name,2025-05-01 00:00:00 +0100,some_user_file_base_name.zip\nsecond_user_file_base_name,2025-05-01 00:00:00 +0100,second_user_file_base_name.zip\nsome_organisation_file_base_name,2025-05-01 00:00:00 +0100,some_organisation_file_base_name.zip\nsecond_organisation_file_base_name,2025-05-01 00:00:00 +0100,second_organisation_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml + expect(actual_content).to eq(expected_content) + end + end + end + end + end + + context "when exporting specific sales log collection" do + let(:start_time) { Time.zone.local(2025, 5, 1) } + let(:expected_master_manifest_filename) { "Manifest_2025_05_01_0001.csv" } + let(:lettings_logs_export_service) { instance_double("Exports::LettingsLogExportService", export_xml_lettings_logs: {}) } + + context "and no sales archives get created in sales logs export" do + let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: {}) } + + context "and user archive gets created in user export" do + let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time }) } + + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml(full_update: true, collection: "sales", year: "2022") + end + + it "does not write user data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml(full_update: true, collection: "sales", year: "2022") + expect(actual_content).to eq(expected_content) + end + end + end + + context "and sales archive gets created in sales logs export" do + let(:sales_logs_export_service) { instance_double("Exports::SalesLogExportService", export_xml_sales_logs: { "some_sales_file_base_name" => start_time }) } + + context "and user archive gets created in user export" do + let(:users_export_service) { instance_double("Exports::UserExportService", export_xml_users: { "some_user_file_base_name" => start_time }) } + + it "generates a master manifest with the correct name" do + expect(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) + export_service.export_xml(full_update: true, collection: "sales", year: "2023") + end + + it "does not write user data" do + actual_content = nil + expected_content = "zip-name,date-time zipped folder generated,zip-file-uri\nsome_sales_file_base_name,2025-05-01 00:00:00 +0100,some_sales_file_base_name.zip\n" + allow(storage_service).to receive(:write_file).with(expected_master_manifest_filename, any_args) { |_, arg2| actual_content = arg2&.string } + + export_service.export_xml(full_update: true, collection: "sales", year: "2023") + expect(actual_content).to eq(expected_content) + end + end + end + end end diff --git a/spec/services/exports/lettings_log_export_service_spec.rb b/spec/services/exports/lettings_log_export_service_spec.rb index 12e3809f0..600590e71 100644 --- a/spec/services/exports/lettings_log_export_service_spec.rb +++ b/spec/services/exports/lettings_log_export_service_spec.rb @@ -448,6 +448,38 @@ RSpec.describe Exports::LettingsLogExportService do end end end + + context "with 25/26 collection period" do + let(:start_time) { Time.zone.local(2025, 4, 3) } + + before do + Timecop.freeze(start_time) + Singleton.__init__(FormHandler) + end + + after do + Timecop.unfreeze + Singleton.__init__(FormHandler) + end + + context "and one lettings log is available for export" do + let!(:lettings_log) { FactoryBot.create(:lettings_log, :completed, startdate: Time.zone.local(2025, 4, 3), assigned_to: user, age1: 35, sex1: "F", age2: 32, sex2: "M", ppostcode_full: "A1 1AA", nationality_all_group: 13, propcode: "123", postcode_full: "SE2 6RT", tenancycode: "BZ737", voiddate: Time.zone.local(2021, 11, 3), mrcdate: Time.zone.local(2022, 5, 5, 10, 36, 49), tenancylength: 5, underoccupation_benefitcap: 4, creation_method: 2, bulk_upload_id: 1, address_line1_as_entered: "address line 1 as entered", address_line2_as_entered: "address line 2 as entered", town_or_city_as_entered: "town or city as entered", county_as_entered: "county as entered", postcode_full_as_entered: "AB1 2CD", la_as_entered: "la as entered", manual_address_entry_selected: false, uprn: "1", uprn_known: 1) } + let(:expected_zip_filename) { "core_2025_2026_apr_mar_f0001_inc0001.zip" } + let(:expected_data_filename) { "core_2025_2026_apr_mar_f0001_inc0001_pt001.xml" } + let(:xml_export_file) { File.open("spec/fixtures/exports/general_needs_log_25_26.xml", "r:UTF-8") } + + it "generates an XML export file with the expected content within the ZIP file" do + expected_content = replace_entity_ids(lettings_log, xml_export_file.read) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_data_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + export_service.export_xml_lettings_logs + end + end + end end context "when exporting a supported housing lettings logs in XML" do diff --git a/spec/services/exports/sales_log_export_service_spec.rb b/spec/services/exports/sales_log_export_service_spec.rb new file mode 100644 index 000000000..c11c13712 --- /dev/null +++ b/spec/services/exports/sales_log_export_service_spec.rb @@ -0,0 +1,425 @@ +require "rails_helper" + +RSpec.describe Exports::SalesLogExportService do + subject(:export_service) { described_class.new(storage_service, start_time) } + + let(:storage_service) { instance_double(Storage::S3Service) } + + let(:xml_export_file) { File.open("spec/fixtures/exports/sales_log.xml", "r:UTF-8") } + let(:local_manifest_file) { File.open("spec/fixtures/exports/manifest.xml", "r:UTF-8") } + + let(:expected_zip_filename) { "core_sales_2025_2026_apr_mar_f0001_inc0001.zip" } + let(:expected_data_filename) { "core_sales_2025_2026_apr_mar_f0001_inc0001_pt001.xml" } + let(:expected_manifest_filename) { "manifest.xml" } + let(:start_time) { Time.zone.local(2026, 3, 1) } + let(:organisation) { create(:organisation, name: "MHCLG", housing_registration_no: 1234) } + let(:user) { FactoryBot.create(:user, email: "test1@example.com", organisation:) } + + def replace_entity_ids(sales_log, export_template) + export_template.sub!(/\{owning_org_id\}/, sales_log["owning_organisation_id"].to_s) + export_template.sub!(/\{owning_org_name\}/, sales_log.owning_organisation.name.to_s) + export_template.sub!(/\{managing_org_id\}/, sales_log["managing_organisation_id"].to_s) + export_template.sub!(/\{managing_org_name\}/, sales_log.managing_organisation.name.to_s) + export_template.sub!(/\{assigned_to_id\}/, sales_log["assigned_to_id"].to_s) + export_template.sub!(/\{assigned_to_email\}/, sales_log.assigned_to&.email.to_s) + export_template.sub!(/\{created_by_id\}/, sales_log["created_by_id"].to_s) + export_template.sub!(/\{created_by_email\}/, sales_log.created_by&.email.to_s) + export_template.sub!(/\{id\}/, sales_log["id"].to_s) + end + + def replace_record_number(export_template, record_number) + export_template.sub!(/\{recno\}/, record_number.to_s) + end + + before do + Timecop.freeze(start_time) + Singleton.__init__(FormHandler) + allow(storage_service).to receive(:write_file) + end + + after do + Timecop.return + end + + context "when exporting daily sales logs in XML" do + context "and no sales logs are available for export" do + it "returns an empty archives list" do + expect(storage_service).not_to receive(:write_file) + expect(export_service.export_xml_sales_logs).to eq({}) + end + end + + context "when one pending sales log exists" do + before do + FactoryBot.create( + :sales_log, + :export, + status: "pending", + skip_update_status: true, + ) + end + + it "returns empty archives list for archives manifest" do + expect(storage_service).not_to receive(:write_file) + expect(export_service.export_xml_sales_logs).to eq({}) + end + end + + context "and one sales log is available for export" do + let!(:sales_log) { FactoryBot.create(:sales_log, :export, assigned_to: user) } + + it "generates a ZIP export file with the expected filename" do + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) + export_service.export_xml_sales_logs + end + + it "generates an XML export file with the expected filename within the ZIP file" do + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_data_filename) + expect(entry).not_to be_nil + expect(entry.name).to eq(expected_data_filename) + end + export_service.export_xml_sales_logs + end + + it "generates an XML manifest file with the expected content within the ZIP file" do + expected_content = replace_record_number(local_manifest_file.read, 1) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_manifest_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + export_service.export_xml_sales_logs + end + + it "generates an XML export file with the expected content within the ZIP file" do + expected_content = replace_entity_ids(sales_log, xml_export_file.read) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_data_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + export_service.export_xml_sales_logs + end + + it "returns the list with correct archive" do + expect(export_service.export_xml_sales_logs).to eq({ expected_zip_filename.gsub(".zip", "") => start_time }) + end + end + + context "and multiple sales logs are available for export on different periods" do + let(:previous_zip_filename) { "core_sales_2024_2025_apr_mar_f0001_inc0001.zip" } + let(:next_zip_filename) { "core_sales_2026_2027_apr_mar_f0001_inc0001.zip" } + + before do + FactoryBot.create(:sales_log, :ignore_validation_errors, saledate: Time.zone.local(2024, 5, 1)) + FactoryBot.create(:sales_log, saledate: Time.zone.local(2025, 5, 1)) + FactoryBot.create(:sales_log, :ignore_validation_errors, saledate: Time.zone.local(2026, 4, 1)) + end + + context "when sales logs are across multiple years" do + it "generates multiple ZIP export files with the expected filenames" do + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) + expect(storage_service).not_to receive(:write_file).with(previous_zip_filename, any_args) + expect(storage_service).to receive(:write_file).with(next_zip_filename, any_args) + expect(Rails.logger).to receive(:info).with("Building export run for sales 2025") + expect(Rails.logger).to receive(:info).with("Creating core_sales_2025_2026_apr_mar_f0001_inc0001 - 1 resources") + expect(Rails.logger).to receive(:info).with("Added core_sales_2025_2026_apr_mar_f0001_inc0001_pt001.xml") + expect(Rails.logger).to receive(:info).with("Writing core_sales_2025_2026_apr_mar_f0001_inc0001.zip") + expect(Rails.logger).to receive(:info).with("Building export run for sales 2026") + expect(Rails.logger).to receive(:info).with("Creating core_sales_2026_2027_apr_mar_f0001_inc0001 - 1 resources") + expect(Rails.logger).to receive(:info).with("Added core_sales_2026_2027_apr_mar_f0001_inc0001_pt001.xml") + expect(Rails.logger).to receive(:info).with("Writing core_sales_2026_2027_apr_mar_f0001_inc0001.zip") + + export_service.export_xml_sales_logs + end + + it "generates zip export files only for specified year" do + expect(storage_service).to receive(:write_file).with(next_zip_filename, any_args) + expect(Rails.logger).to receive(:info).with("Building export run for sales 2026") + expect(Rails.logger).to receive(:info).with("Creating core_sales_2026_2027_apr_mar_f0001_inc0001 - 1 resources") + expect(Rails.logger).to receive(:info).with("Added core_sales_2026_2027_apr_mar_f0001_inc0001_pt001.xml") + expect(Rails.logger).to receive(:info).with("Writing core_sales_2026_2027_apr_mar_f0001_inc0001.zip") + + export_service.export_xml_sales_logs(collection_year: 2026) + end + + context "and previous full exports are different for previous years" do + let(:expected_zip_filename) { "core_sales_2025_2026_apr_mar_f0007_inc0004.zip" } + let(:next_zip_filename) { "core_sales_2026_2027_apr_mar_f0001_inc0001.zip" } + + before do + Export.new(started_at: Time.zone.yesterday, base_number: 7, increment_number: 3, collection: "sales", year: 2025).save! + end + + it "generates multiple ZIP export files with different base numbers in the filenames" do + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) + expect(storage_service).to receive(:write_file).with(next_zip_filename, any_args) + expect(Rails.logger).to receive(:info).with("Building export run for sales 2025") + expect(Rails.logger).to receive(:info).with("Creating core_sales_2025_2026_apr_mar_f0007_inc0004 - 1 resources") + expect(Rails.logger).to receive(:info).with("Added core_sales_2025_2026_apr_mar_f0007_inc0004_pt001.xml") + expect(Rails.logger).to receive(:info).with("Writing core_sales_2025_2026_apr_mar_f0007_inc0004.zip") + expect(Rails.logger).to receive(:info).with("Building export run for sales 2026") + expect(Rails.logger).to receive(:info).with("Creating core_sales_2026_2027_apr_mar_f0001_inc0001 - 1 resources") + expect(Rails.logger).to receive(:info).with("Added core_sales_2026_2027_apr_mar_f0001_inc0001_pt001.xml") + expect(Rails.logger).to receive(:info).with("Writing core_sales_2026_2027_apr_mar_f0001_inc0001.zip") + + export_service.export_xml_sales_logs + end + end + end + end + + context "and multiple sales logs are available for export on same quarter" do + before do + FactoryBot.create(:sales_log, saledate: Time.zone.local(2025, 4, 1)) + FactoryBot.create(:sales_log, saledate: Time.zone.local(2025, 4, 20)) + end + + it "generates an XML manifest file with the expected content within the ZIP file" do + expected_content = replace_record_number(local_manifest_file.read, 2) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_manifest_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + export_service.export_xml_sales_logs + end + + it "creates a logs export record in a database with correct time" do + expect { export_service.export_xml_sales_logs } + .to change(Export, :count).by(2) + expect(Export.last.started_at).to be_within(2.seconds).of(start_time) + end + + context "when this is the first export (full)" do + it "returns a ZIP archive for the master manifest (existing sales logs)" do + expect(export_service.export_xml_sales_logs).to eq({ expected_zip_filename.gsub(".zip", "").gsub(".zip", "") => start_time }) + end + end + + context "and underlying data changes between getting the logs and writting the manifest" do + before do + FactoryBot.create(:sales_log, saledate: Time.zone.local(2026, 2, 1)) + FactoryBot.create(:sales_log, :ignore_validation_errors, saledate: Time.zone.local(2026, 4, 1)) + end + + def remove_logs(logs) + logs.each(&:destroy) + file = Tempfile.new + doc = Nokogiri::XML("") + doc.write_xml_to(file, encoding: "UTF-8") + file.rewind + file + end + + def create_fake_maifest + file = Tempfile.new + doc = Nokogiri::XML("") + doc.write_xml_to(file, encoding: "UTF-8") + file.rewind + file + end + + it "maintains the same record number" do + # rubocop:disable RSpec/SubjectStub + allow(export_service).to receive(:build_export_xml) do |logs| + remove_logs(logs) + end + allow(export_service).to receive(:build_manifest_xml) do + create_fake_maifest + end + + expect(export_service).to receive(:build_manifest_xml).with(1) + # rubocop:enable RSpec/SubjectStub + export_service.export_xml_sales_logs + end + end + + context "when this is a second export (partial)" do + before do + start_time = Time.zone.local(2026, 6, 1) + Export.new(started_at: start_time, collection: "sales", year: 2025).save! + end + + it "does not add any entry for the master manifest (no sales logs)" do + expect(storage_service).not_to receive(:write_file) + expect(export_service.export_xml_sales_logs).to eq({}) + end + end + end + + context "and a previous export has run the same day having sales logs" do + before do + FactoryBot.create(:sales_log, saledate: Time.zone.local(2025, 5, 1)) + export_service.export_xml_sales_logs + end + + context "and we trigger another full update" do + it "increments the base number" do + export_service.export_xml_sales_logs(full_update: true) + expect(Export.last.base_number).to eq(2) + end + + it "resets the increment number" do + export_service.export_xml_sales_logs(full_update: true) + expect(Export.last.increment_number).to eq(1) + end + + it "returns a correct archives list for manifest file" do + expect(export_service.export_xml_sales_logs(full_update: true)).to eq({ "core_sales_2025_2026_apr_mar_f0002_inc0001" => start_time }) + end + + it "generates a ZIP export file with the expected filename" do + expect(storage_service).to receive(:write_file).with("core_sales_2025_2026_apr_mar_f0002_inc0001.zip", any_args) + export_service.export_xml_sales_logs(full_update: true) + end + end + end + + context "and a previous export has run having no sales logs" do + before { export_service.export_xml_sales_logs } + + it "doesn't increment the manifest number by 1" do + export_service.export_xml_sales_logs + + expect(Export.last.increment_number).to eq(1) + end + end + + context "and a log has been manually updated since the previous partial export" do + let(:expected_zip_filename) { "core_sales_2025_2026_apr_mar_f0001_inc0002.zip" } + + before do + FactoryBot.create(:sales_log, saledate: Time.zone.local(2026, 2, 1), updated_at: Time.zone.local(2026, 2, 27), values_updated_at: Time.zone.local(2026, 2, 29)) + FactoryBot.create(:sales_log, saledate: Time.zone.local(2026, 2, 1), updated_at: Time.zone.local(2026, 2, 27), values_updated_at: Time.zone.local(2026, 2, 29)) + Export.create!(started_at: Time.zone.local(2026, 2, 28), base_number: 1, increment_number: 1, collection: "sales", year: 2025) + end + + it "generates an XML manifest file with the expected content within the ZIP file" do + expected_content = replace_record_number(local_manifest_file.read, 2) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_manifest_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + expect(export_service.export_xml_sales_logs).to eq({ expected_zip_filename.gsub(".zip", "") => start_time }) + end + end + + context "and one sales log with duplicate reference is available for export" do + let!(:sales_log) { FactoryBot.create(:sales_log, :export, duplicate_set_id: 123) } + + def replace_duplicate_set_id(export_file) + export_file.sub!("", "123") + end + + it "generates an XML export file with the expected content within the ZIP file" do + expected_content = replace_entity_ids(sales_log, xml_export_file.read) + expected_content = replace_duplicate_set_id(expected_content) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_data_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + export_service.export_xml_sales_logs + end + end + + context "when exporting only 24/25 collection period" do + let(:start_time) { Time.zone.local(2024, 4, 3) } + + before do + Timecop.freeze(start_time) + Singleton.__init__(FormHandler) + end + + after do + Timecop.unfreeze + Singleton.__init__(FormHandler) + end + + context "and one sales log is available for export" do + let!(:sales_log) { FactoryBot.create(:sales_log, :export) } + let(:expected_zip_filename) { "core_sales_2024_2025_apr_mar_f0001_inc0001.zip" } + let(:expected_data_filename) { "core_sales_2024_2025_apr_mar_f0001_inc0001_pt001.xml" } + let(:xml_export_file) { File.open("spec/fixtures/exports/sales_log_2024.xml", "r:UTF-8") } + + it "generates an XML export file with the expected content within the ZIP file" do + expected_content = replace_entity_ids(sales_log, xml_export_file.read) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_data_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + export_service.export_xml_sales_logs(full_update: true, collection_year: 2024) + end + end + end + + context "when exporting various fees, correctly maps the values" do + context "with discounted ownership and mscharge" do + let!(:sales_log) { FactoryBot.create(:sales_log, :export, mscharge: 123) } + + def replace_mscharge_value(export_file) + export_file.sub!("100.0", "123.0") + end + + it "exports mscharge fields as hasmscharge and mscharge" do + expected_content = replace_entity_ids(sales_log, xml_export_file.read) + expected_content = replace_mscharge_value(expected_content) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_data_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + export_service.export_xml_sales_logs + end + end + + context "with shared ownership and mscharge" do + let!(:sales_log) { FactoryBot.create(:sales_log, :export, ownershipsch: 1, staircase: 2, type: 30, mscharge: 321, has_management_fee: 1, management_fee: 222) } + + def replace_mscharge_and_shared_ownership_values(export_file) + export_file.sub!("", "1") + export_file.sub!("", "321.0") + export_file.sub!("", "1") + export_file.sub!("", "222.0") + export_file.sub!("100.0", "") + export_file.sub!("1", "") + + export_file.sub!("8", "30") + export_file.sub!("", "2") + export_file.sub!("10000.0", "") + export_file.sub!("0", "1") + export_file.sub!("SW1A", "") + export_file.sub!("1AA", "") + export_file.sub!("E09000033", "") + export_file.sub!("1", "") + export_file.sub!("2", "1") + export_file.sub!("1", "0") + export_file.sub!("Westminster", "") + end + + it "exports mscharge fields as hasmscharge and mscharge" do + expected_content = replace_entity_ids(sales_log, xml_export_file.read) + expected_content = replace_mscharge_and_shared_ownership_values(expected_content) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_data_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + export_service.export_xml_sales_logs + end + end + end + end +end diff --git a/spec/services/exports/user_export_service_spec.rb b/spec/services/exports/user_export_service_spec.rb index 3f4ece76d..93ba124d9 100644 --- a/spec/services/exports/user_export_service_spec.rb +++ b/spec/services/exports/user_export_service_spec.rb @@ -202,7 +202,26 @@ RSpec.describe Exports::UserExportService do before do create(:user, updated_at: Time.zone.local(2022, 4, 27), organisation:) create(:user, updated_at: Time.zone.local(2022, 4, 27), organisation:) - Export.create!(started_at: Time.zone.local(2022, 4, 26), base_number: 1, increment_number: 1) + Export.create!(started_at: Time.zone.local(2022, 4, 26), base_number: 1, increment_number: 1, empty_export: true, collection: "users") + end + + it "generates an XML manifest file with the expected content within the ZIP file" do + expected_content = replace_record_number(local_manifest_file.read, 2) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_manifest_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + expect(export_service.export_xml_users).to eq({ expected_zip_filename.gsub(".zip", "") => start_time }) + end + end + + context "and a user has been manually updated since the previous partial export" do + before do + create(:user, updated_at: Time.zone.local(2022, 4, 25), values_updated_at: Time.zone.local(2022, 4, 27), organisation:) + create(:user, updated_at: Time.zone.local(2022, 4, 25), values_updated_at: Time.zone.local(2022, 4, 27), organisation:) + Export.create!(started_at: Time.zone.local(2022, 4, 26), base_number: 1, increment_number: 1, empty_export: true, collection: "users") end it "generates an XML manifest file with the expected content within the ZIP file" do diff --git a/spec/services/merge/merge_organisations_service_spec.rb b/spec/services/merge/merge_organisations_service_spec.rb index f20e16a16..9355a757a 100644 --- a/spec/services/merge/merge_organisations_service_spec.rb +++ b/spec/services/merge/merge_organisations_service_spec.rb @@ -31,10 +31,12 @@ RSpec.describe Merge::MergeOrganisationsService do expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("New schemes from fake org:") + expect(merging_organisation_user.values_updated_at).to be_nil merge_organisations_service.call merging_organisation_user.reload expect(merging_organisation_user.organisation).to eq(absorbing_organisation) + expect(merging_organisation_user.values_updated_at).not_to be_nil end it "sets merge date on merged organisation" do diff --git a/spec/services/uprn_client_spec.rb b/spec/services/uprn_client_spec.rb index b10ab7376..8d5c45ed9 100644 --- a/spec/services/uprn_client_spec.rb +++ b/spec/services/uprn_client_spec.rb @@ -86,5 +86,33 @@ describe UprnClient do expect(client.error).to be_nil end end + + describe "result" do + context "when response is successful" do + before do + stub_api_request(body: valid_response) + + client.call + end + + it "returns parsed result" do + expect(client.result).to eq({ "postcode" => "12345" }) + expect(client.send(:response).code.to_i).to eq(200) + end + end + + context "when response is not successful" do + before do + stub_api_request(body: valid_response, status: 500) + + client.call + end + + it "returns nil" do + expect(client.result).to be_nil + expect(client.error).to eq("UPRN client failed to return a valid result, try again later.") + end + end + end end end diff --git a/yarn.lock b/yarn.lock index 05279f9ed..5b3865609 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,6 +18,15 @@ "@babel/highlight" "^7.24.7" picocolors "^1.0.0" +"@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.2": version "7.25.2" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.2.tgz#e41928bd33475305c586f6acbbb7e3ade7a6f7f5" @@ -190,11 +199,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.24.8": version "7.24.8" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" @@ -210,12 +229,12 @@ "@babel/types" "^7.25.0" "@babel/helpers@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.0.tgz#e69beb7841cb93a6505531ede34f34e6a073650a" - integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw== + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" + integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== dependencies: - "@babel/template" "^7.25.0" - "@babel/types" "^7.25.0" + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.10" "@babel/highlight@^7.24.7": version "7.24.7" @@ -234,6 +253,13 @@ dependencies: "@babel/types" "^7.25.2" +"@babel/parser@^7.26.9": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749" + integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA== + dependencies: + "@babel/types" "^7.26.10" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.3": version "7.25.3" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz#dca427b45a6c0f5c095a1c639dfe2476a3daba7f" @@ -913,24 +939,10 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.12.5": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" - integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.16.7": - version "7.24.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.6.tgz#5b76eb89ad45e2e4a0a8db54c456251469a3358e" - integrity sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/runtime@^7.8.4": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb" - integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== +"@babel/runtime@^7.12.5", "@babel/runtime@^7.16.7", "@babel/runtime@^7.8.4": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" + integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== dependencies: regenerator-runtime "^0.14.0" @@ -943,6 +955,15 @@ "@babel/parser" "^7.25.0" "@babel/types" "^7.25.0" +"@babel/template@^7.26.9": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2" + integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.26.9" + "@babel/types" "^7.26.9" + "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2", "@babel/traverse@^7.25.3": version "7.25.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.3.tgz#f1b901951c83eda2f3e29450ce92743783373490" @@ -965,6 +986,14 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" +"@babel/types@^7.26.10", "@babel/types@^7.26.9": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259" + integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@csstools/css-parser-algorithms@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.0.tgz#dd428c4569caea7185716fbba174202a4ba41fda" @@ -3820,7 +3849,7 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: +regenerator-runtime@^0.13.9: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==