diff --git a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb index 042ec132a..a2d59a370 100644 --- a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb +++ b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb @@ -12,6 +12,8 @@ module Forms case year when 2023 "bulk_upload_lettings_logs/forms/prepare_your_file_2023" + when 2024 + "bulk_upload_lettings_logs/forms/prepare_your_file_2024" end end diff --git a/app/services/bulk_upload/lettings/log_creator.rb b/app/services/bulk_upload/lettings/log_creator.rb index dd8820b35..a81b6260d 100644 --- a/app/services/bulk_upload/lettings/log_creator.rb +++ b/app/services/bulk_upload/lettings/log_creator.rb @@ -33,6 +33,8 @@ private @csv_parser ||= case bulk_upload.year when 2023 BulkUpload::Lettings::Year2023::CsvParser.new(path:) + when 2024 + BulkUpload::Lettings::Year2024::CsvParser.new(path:) else raise "csv parser not found" end diff --git a/app/services/bulk_upload/lettings/validator.rb b/app/services/bulk_upload/lettings/validator.rb index 59d837acf..1eb98f030 100644 --- a/app/services/bulk_upload/lettings/validator.rb +++ b/app/services/bulk_upload/lettings/validator.rb @@ -100,6 +100,8 @@ private @csv_parser ||= case bulk_upload.year when 2023 BulkUpload::Lettings::Year2023::CsvParser.new(path:) + when 2024 + BulkUpload::Lettings::Year2024::CsvParser.new(path:) else raise "csv parser not found" end diff --git a/app/services/bulk_upload/lettings/year2023/csv_parser.rb b/app/services/bulk_upload/lettings/year2023/csv_parser.rb index a51f215fb..2b80c6f81 100644 --- a/app/services/bulk_upload/lettings/year2023/csv_parser.rb +++ b/app/services/bulk_upload/lettings/year2023/csv_parser.rb @@ -105,9 +105,9 @@ private def first_record_start_date if with_headers? - Date.new(row_parsers.first.field_98.to_i + 2000, row_parsers.first.field_97.to_i, row_parsers.first.field_96.to_i) + Date.new(row_parsers.first.field_9.to_i + 2000, row_parsers.first.field_8.to_i, row_parsers.first.field_7.to_i) else - Date.new(rows.first[97].to_i + 2000, rows.first[96].to_i, rows.first[95].to_i) + Date.new(rows.first[8].to_i + 2000, rows.first[7].to_i, rows.first[6].to_i) end end end diff --git a/app/services/bulk_upload/lettings/year2024/csv_parser.rb b/app/services/bulk_upload/lettings/year2024/csv_parser.rb new file mode 100644 index 000000000..1fb083c3e --- /dev/null +++ b/app/services/bulk_upload/lettings/year2024/csv_parser.rb @@ -0,0 +1,113 @@ +require "csv" + +class BulkUpload::Lettings::Year2024::CsvParser + include CollectionTimeHelper + + FIELDS = 130 + MAX_COLUMNS = 131 + FORM_YEAR = 2024 + + attr_reader :path + + def initialize(path:) + @path = path + end + + def row_offset + if with_headers? + rows.find_index { |row| row[0].match(/field number/i) } + 1 + else + 0 + end + end + + def col_offset + with_headers? ? 1 : 0 + end + + def cols + @cols ||= ("A".."EA").to_a + end + + def row_parsers + @row_parsers ||= body_rows.map do |row| + stripped_row = row[col_offset..] + hash = Hash[field_numbers.zip(stripped_row)] + + BulkUpload::Lettings::Year2024::RowParser.new(hash) + end + end + + def body_rows + rows[row_offset..] + end + + def rows + @rows ||= CSV.parse(normalised_string, row_sep:) + end + + def column_for_field(field) + cols[field_numbers.find_index(field) + col_offset] + end + + def correct_field_count? + valid_field_numbers_count = field_numbers.count { |f| f != "field_blank" } + + valid_field_numbers_count == FIELDS + end + + def too_many_columns? + return if with_headers? + + max_columns_count = body_rows.map(&:size).max - col_offset + + max_columns_count > MAX_COLUMNS + end + + def wrong_template_for_year? + collection_start_year_for_date(first_record_start_date) != FORM_YEAR + rescue Date::Error + false + end + +private + + def default_field_numbers + (1..FIELDS).map { |h| h.present? && h.to_s.match?(/^[0-9]+$/) ? "field_#{h}" : "field_blank" } + end + + def field_numbers + @field_numbers ||= if with_headers? + rows[row_offset - 1][col_offset..].map { |h| h.present? && h.match?(/^[0-9]+$/) ? "field_#{h}" : "field_blank" } + else + default_field_numbers + end + end + + def with_headers? + rows.map { |r| r[0] }.any? { |cell| cell&.match?(/field number/i) } + end + + def row_sep + "\n" + end + + def normalised_string + return @normalised_string if @normalised_string + + @normalised_string = File.read(path, encoding: "bom|utf-8") + @normalised_string.gsub!("\r\n", "\n") + @normalised_string.scrub!("") + @normalised_string.tr!("\r", "\n") + + @normalised_string + end + + def first_record_start_date + if with_headers? + Date.new(row_parsers.first.field_10.to_i + 2000, row_parsers.first.field_9.to_i, row_parsers.first.field_8.to_i) + else + Date.new(rows.first[9].to_i + 2000, rows.first[8].to_i, rows.first[7].to_i) + end + end +end diff --git a/app/services/bulk_upload/lettings/year2024/row_parser.rb b/app/services/bulk_upload/lettings/year2024/row_parser.rb new file mode 100644 index 000000000..19bae0ecc --- /dev/null +++ b/app/services/bulk_upload/lettings/year2024/row_parser.rb @@ -0,0 +1,1469 @@ +class BulkUpload::Lettings::Year2024::RowParser + include ActiveModel::Model + include ActiveModel::Attributes + include InterruptionScreenHelper + + QUESTIONS = { + field_1: "Which organisation owns this property?", + field_2: "Which organisation manages this letting?", + field_3: "What is the CORE username of the account this letting log should be assigned to?", + field_4: "What is the needs type?", + field_7: "Is this letting a renewal?", + field_8: "What is the tenancy start date?", + field_9: "What is the tenancy start date?", + field_10: "What is the tenancy start date?", + field_11: "Is this a London Affordable Rent letting?", + field_12: "Which 'Other' type of Intermediate Rent is this letting?", + field_13: "What is the tenant code?", + field_14: "What is the property reference?", + field_5: "What scheme does this letting belong to?", + field_6: "Which location is this letting for?", + field_16: "If known, provide this property’s UPRN", + field_17: "Address line 1", + field_18: "Address line 2", + field_19: "Town or city", + field_20: "County", + field_21: "Part 1 of the property's postcode", + field_22: "Part 2 of the property's postcode", + field_23: "What is the property's local authority?", + field_25: "What type was the property most recently let as?", + field_24: "What is the reason for the property being vacant?", + field_26: "What type of unit is the property?", + field_27: "Which type of building is the property?", + field_28: "Is the property built or adapted to wheelchair-user standards?", + field_29: "How many bedrooms does the property have?", + field_30: "What is the void date?", + field_31: "What is the void date?", + field_32: "What is the void date?", + field_33: "What date were any major repairs completed on?", + field_34: "What date were any major repairs completed on?", + field_35: "What date were any major repairs completed on?", + field_36: "Is this a joint tenancy?", + field_37: "Is this a starter tenancy?", + field_38: "What is the type of tenancy?", + field_39: "If 'Other', what is the type of tenancy?", + field_40: "What is the length of the fixed-term tenancy to the nearest year?", + field_41: "Is this letting sheltered accommodation?", + field_15: "Has tenant seen the DLUHC privacy notice?", + field_42: "What is the lead tenant's age?", + field_43: "Which of these best describes the lead tenant's gender identity?", + field_44: "Which of these best describes the lead tenant's ethnic background?", + field_45: "What is the lead tenant's nationality?", + field_46: "Which of these best describes the lead tenant's working situation?", + field_47: "What is person 2's relationship to the lead tenant?", + field_48: "What is person 2's age?", + field_49: "Which of these best describes person 2's gender identity?", + field_50: "Which of these best describes person 2's working situation?", + field_51: "What is person 3's relationship to the lead tenant?", + field_52: "What is person 3's age?", + field_53: "Which of these best describes person 3's gender identity?", + field_54: "Which of these best describes person 3's working situation?", + field_55: "What is person 4's relationship to the lead tenant?", + field_56: "What is person 4's age?", + field_57: "Which of these best describes person 4's gender identity?", + field_58: "Which of these best describes person 4's working situation?", + field_59: "What is person 5's relationship to the lead tenant?", + field_60: "What is person 5's age?", + field_61: "Which of these best describes person 5's gender identity?", + field_62: "Which of these best describes person 5's working situation?", + field_63: "What is person 6's relationship to the lead tenant?", + field_64: "What is person 6's age?", + field_65: "Which of these best describes person 6's gender identity?", + field_66: "Which of these best describes person 6's working situation?", + field_67: "What is person 7's relationship to the lead tenant?", + field_68: "What is person 7's age?", + field_69: "Which of these best describes person 7's gender identity?", + field_70: "Which of these best describes person 7's working situation?", + field_71: "What is person 8's relationship to the lead tenant?", + field_72: "What is person 8's age?", + field_73: "Which of these best describes person 8's gender identity?", + field_74: "Which of these best describes person 8's working situation?", + field_75: "Does anybody in the household have links to the UK armed forces?", + field_76: "Is this person still serving in the UK armed forces?", + field_77: "Was this person seriously injured or ill as a result of serving in the UK armed forces?", + field_78: "Is anybody in the household pregnant?", + field_79: "Does anybody in the household have any disabled access needs?", + field_80: "Does anybody in the household have any disabled access needs?", + field_81: "Does anybody in the household have any disabled access needs?", + field_82: "Does anybody in the household have any disabled access needs?", + field_83: "Does anybody in the household have any disabled access needs?", + field_84: "Does anybody in the household have any disabled access needs?", + field_85: "Does anybody in the household have a physical or mental health condition (or other illness) expected to last 12 months or more?", + field_86: "Does this person's condition affect their dexterity?", + field_87: "Does this person's condition affect their learning or understanding or concentrating?", + field_88: "Does this person's condition affect their hearing?", + field_89: "Does this person's condition affect their memory?", + field_90: "Does this person's condition affect their mental health?", + field_91: "Does this person's condition affect their mobility?", + field_92: "Does this person's condition affect them socially or behaviourally?", + field_93: "Does this person's condition affect their stamina or breathing or fatigue?", + field_94: "Does this person's condition affect their vision?", + field_95: "Does this person's condition affect them in another way?", + field_96: "How long has the household continuously lived in the local authority area of the new letting?", + field_97: "How long has the household been on the local authority waiting list for the new letting?", + field_98: "What is the tenant’s main reason for the household leaving their last settled home?", + field_99: "If 'Other', what was the main reason for leaving their last settled home?", + field_100: "Where was the household immediately before this letting?", + field_101: "Did the household experience homelessness immediately before this letting?", + field_102: "Do you know the postcode of the household's last settled home?", + field_103: "What is the postcode of the household's last settled home?", + field_104: "What is the postcode of the household's last settled home?", + field_105: "What is the local authority of the household's last settled home?", + field_106: "Was the household given 'reasonable preference' by the local authority?", + field_107: "Reasonable preference reason They were homeless or about to lose their home (within 56 days)", + field_108: "Reasonable preference reason They were living in insanitary, overcrowded or unsatisfactory housing", + field_109: "Reasonable preference reason They needed to move on medical and welfare reasons (including disability)", + field_110: "Reasonable preference reason They needed to move to avoid hardship to themselves or others", + field_111: "Reasonable preference reason Don't know", + field_112: "Was the letting made under the Choice-Based Lettings (CBL)?", + field_113: "Was the letting made under the Common Allocation Policy (CAP)?", + field_114: "Was the letting made under the Common Housing Register (CHR)?", + field_115: "Was the letting made under the Accessible Register?", + field_116: "What was the source of referral for this letting?", + field_117: "Do you know the household's combined total income after tax?", + field_119: "How often does the household receive income?", + field_118: "How much income does the household have in total?", + field_120: "Is the tenant likely to be receiving any of these housing-related benefits?", + field_121: "How much of the household's income is from Universal Credit, state pensions or benefits?", + field_122: "Does the household pay rent or other charges for the accommodation?", + field_123: "How often does the household pay rent and other charges?", + field_124: "If this is a care home, how much does the household pay every [time period]?", + field_125: "What is the basic rent?", + field_126: "What is the service charge?", + field_127: "What is the personal service charge?", + field_128: "What is the support charge?", + field_129: "After the household has received any housing-related benefits, will they still need to pay for rent and charges?", + field_130: "What do you expect the outstanding amount to be?", + }.freeze + + attribute :bulk_upload + attribute :block_log_creation, :boolean, default: -> { false } + + attribute :field_blank + + attribute :field_1, :string + attribute :field_2, :string + attribute :field_3, :string + attribute :field_4, :integer + attribute :field_7, :integer + attribute :field_8, :integer + attribute :field_9, :integer + attribute :field_10, :integer + attribute :field_11, :integer + attribute :field_12, :string + attribute :field_13, :string + attribute :field_14, :string + attribute :field_5, :string + attribute :field_6, :string + attribute :field_16, :string + attribute :field_17, :string + attribute :field_18, :string + attribute :field_19, :string + attribute :field_20, :string + attribute :field_21, :string + attribute :field_22, :string + attribute :field_23, :string + attribute :field_25, :integer + attribute :field_24, :integer + attribute :field_26, :integer + attribute :field_27, :integer + attribute :field_28, :integer + attribute :field_29, :integer + attribute :field_30, :integer + attribute :field_31, :integer + attribute :field_32, :integer + attribute :field_33, :integer + attribute :field_34, :integer + attribute :field_35, :integer + attribute :field_36, :integer + attribute :field_37, :integer + attribute :field_38, :integer + attribute :field_39, :string + attribute :field_40, :integer + attribute :field_41, :integer + attribute :field_15, :integer + attribute :field_42, :string + attribute :field_43, :string + attribute :field_44, :integer + attribute :field_45, :integer + attribute :field_46, :integer + attribute :field_47, :string + attribute :field_48, :string + attribute :field_49, :string + attribute :field_50, :integer + attribute :field_51, :string + attribute :field_52, :string + attribute :field_53, :string + attribute :field_54, :integer + attribute :field_55, :string + attribute :field_56, :string + attribute :field_57, :string + attribute :field_58, :integer + attribute :field_59, :string + attribute :field_60, :string + attribute :field_61, :string + attribute :field_62, :integer + attribute :field_63, :string + attribute :field_64, :string + attribute :field_65, :string + attribute :field_66, :integer + attribute :field_67, :string + attribute :field_68, :string + attribute :field_69, :string + attribute :field_70, :integer + attribute :field_71, :string + attribute :field_72, :string + attribute :field_73, :string + attribute :field_74, :integer + attribute :field_75, :integer + attribute :field_76, :integer + attribute :field_77, :integer + attribute :field_78, :integer + attribute :field_79, :integer + attribute :field_80, :integer + attribute :field_81, :integer + attribute :field_82, :integer + attribute :field_83, :integer + attribute :field_84, :integer + attribute :field_85, :integer + attribute :field_86, :integer + attribute :field_87, :integer + attribute :field_88, :integer + attribute :field_89, :integer + attribute :field_90, :integer + attribute :field_91, :integer + attribute :field_92, :integer + attribute :field_93, :integer + attribute :field_94, :integer + attribute :field_95, :integer + attribute :field_96, :integer + attribute :field_97, :integer + attribute :field_98, :integer + attribute :field_99, :string + attribute :field_100, :integer + attribute :field_101, :integer + attribute :field_102, :integer + attribute :field_103, :string + attribute :field_104, :string + attribute :field_105, :string + attribute :field_106, :integer + attribute :field_107, :integer + attribute :field_108, :integer + attribute :field_109, :integer + attribute :field_110, :integer + attribute :field_111, :integer + attribute :field_112, :integer + attribute :field_113, :integer + attribute :field_114, :integer + attribute :field_115, :integer + attribute :field_116, :integer + attribute :field_117, :integer + attribute :field_119, :integer + attribute :field_118, :decimal + attribute :field_120, :integer + attribute :field_121, :integer + attribute :field_122, :integer + attribute :field_123, :integer + attribute :field_124, :decimal + attribute :field_125, :decimal + attribute :field_126, :decimal + attribute :field_127, :decimal + attribute :field_128, :decimal + attribute :field_129, :integer + attribute :field_130, :decimal + + validate :validate_valid_radio_option, on: :before_log + + validates :field_11, + presence: { + message: I18n.t("validations.not_answered", question: "rent type"), + category: :setup, + }, + inclusion: { + in: (0..5).to_a, + message: I18n.t("validations.invalid_option", question: "rent type"), + unless: -> { field_11.blank? }, + category: :setup, + }, + on: :after_log + + validates :field_7, + presence: { + message: I18n.t("validations.not_answered", question: "property renewal"), + category: :setup, + }, + on: :after_log + + validates :field_8, + presence: { + message: I18n.t("validations.not_answered", question: "tenancy start date (day)"), + category: :setup, + }, + on: :after_log + + validates :field_9, + presence: { + message: I18n.t("validations.not_answered", question: "tenancy start date (month)"), + category: :setup, + }, + on: :after_log + + validates :field_10, + presence: { + message: I18n.t("validations.not_answered", question: "tenancy start date (year)"), + category: :setup, + }, + format: { + with: /\A\d{2}\z/, + message: I18n.t("validations.setup.startdate.year_not_two_digits"), + category: :setup, + unless: -> { field_10.blank? }, + }, + on: :after_log + + validates :field_5, + presence: { + if: proc { supported_housing? }, + message: I18n.t("validations.not_answered", question: "scheme code"), + category: :setup, + }, + on: :after_log + + validates :field_6, + presence: { + if: proc { supported_housing? }, + message: I18n.t("validations.not_answered", question: "location code"), + category: :setup, + }, + on: :after_log + + validates :field_42, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 1 must be a number or the letter R" }, on: :after_log + validates :field_48, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 2 must be a number or the letter R" }, on: :after_log, if: proc { details_known?(2).zero? } + validates :field_52, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 3 must be a number or the letter R" }, on: :after_log, if: proc { details_known?(3).zero? } + validates :field_56, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 4 must be a number or the letter R" }, on: :after_log, if: proc { details_known?(4).zero? } + validates :field_60, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 5 must be a number or the letter R" }, on: :after_log, if: proc { details_known?(5).zero? } + validates :field_64, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 6 must be a number or the letter R" }, on: :after_log, if: proc { details_known?(6).zero? } + validates :field_68, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 7 must be a number or the letter R" }, on: :after_log, if: proc { details_known?(7).zero? } + validates :field_72, format: { with: /\A\d{1,3}\z|\AR\z/, message: "Age of person 8 must be a number or the letter R" }, on: :after_log, if: proc { details_known?(8).zero? } + + validate :validate_needs_type_present, on: :after_log + validate :validate_data_types, on: :after_log + validate :validate_relevant_collection_window, on: :after_log + validate :validate_la_with_local_housing_referral, on: :after_log + validate :validate_cannot_be_la_referral_if_general_needs_and_la, on: :after_log + validate :validate_leaving_reason_for_renewal, on: :after_log + validate :validate_only_one_housing_needs_type, on: :after_log + validate :validate_no_disabled_needs_conjunction, on: :after_log + validate :validate_dont_know_disabled_needs_conjunction, on: :after_log + validate :validate_no_and_dont_know_disabled_needs_conjunction, on: :after_log + validate :validate_no_housing_needs_questions_answered, on: :after_log + validate :validate_reasonable_preference_homeless, on: :after_log + validate :validate_condition_effects, on: :after_log + validate :validate_lettings_allocation, on: :after_log + validate :validate_if_log_already_exists, on: :after_log, if: -> { FeatureToggle.bulk_upload_duplicate_log_check_enabled? } + + validate :validate_owning_org_data_given, on: :after_log + validate :validate_owning_org_exists, on: :after_log + validate :validate_owning_org_owns_stock, on: :after_log + validate :validate_owning_org_permitted, on: :after_log + + validate :validate_managing_org_data_given, on: :after_log + validate :validate_managing_org_exists, on: :after_log + validate :validate_managing_org_related, on: :after_log + + validate :validate_related_scheme_exists, on: :after_log + validate :validate_related_location_exists, on: :after_log + + validate :validate_created_by_exists, on: :after_log + validate :validate_created_by_related, on: :after_log + + validate :validate_declaration_acceptance, on: :after_log + + validate :validate_nulls, on: :after_log + + validate :validate_uprn_exists_if_any_key_address_fields_are_blank, on: :after_log, unless: -> { supported_housing? } + + validate :validate_incomplete_soft_validations, on: :after_log + + def self.question_for_field(field) + QUESTIONS[field] + end + + def valid? + return @valid if @valid + + errors.clear + + return @valid = true if blank_row? + + super(:before_log) + before_errors = errors.dup + + log.valid? + + super(:after_log) + errors.merge!(before_errors) + + log.errors.each do |error| + fields = field_mapping_for_errors[error.attribute] || [] + + fields.each do |field| + next if errors.include?(field) + + question = log.form.get_question(error.attribute, log) + + if question.present? && setup_question?(question) + errors.add(field, error.message, category: :setup) + else + errors.add(field, error.message) + end + end + end + + @valid = errors.blank? + end + + def blank_row? + attribute_set + .to_hash + .reject { |k, _| %w[bulk_upload block_log_creation field_blank].include?(k) } + .values + .reject(&:blank?) + .compact + .empty? + end + + def log + @log ||= LettingsLog.new(attributes_for_log) + end + + def block_log_creation! + self.block_log_creation = true + end + + def block_log_creation? + block_log_creation + end + + def tenant_code + field_13 + end + + def property_ref + field_14 + end + + def log_already_exists? + @log_already_exists ||= LettingsLog + .where(status: %w[not_started in_progress completed]) + .exists?(duplicate_check_fields.index_with { |field| log.public_send(field) }) + end + + def spreadsheet_duplicate_hash + hash = attributes.slice( + "field_1", # owning org + "field_8", # startdate + "field_9", # startdate + "field_10", # startdate + "field_13", # tenancycode + !general_needs? ? :field_6.to_s : nil, # location + !supported_housing? ? "field_21" : nil, # postcode + !supported_housing? ? "field_22" : nil, # postcode + "field_42", # age1 + "field_43", # sex1 + "field_46", # ecstat1 + ) + if [field_125, field_126, field_127, field_128].all?(&:present?) + hash.merge({ "tcharge" => [field_125, field_126, field_127, field_128].sum }) + else + hash + end + end + + def add_duplicate_found_in_spreadsheet_errors + spreadsheet_duplicate_hash.each_key do |field| + if field == "tcharge" + %w[field_125 field_126 field_127 field_128].each do |sub_field| + errors.add(sub_field, :spreadsheet_dupe, category: :setup) + end + else + errors.add(field, :spreadsheet_dupe, category: :setup) + end + end + end + +private + + def validate_declaration_acceptance + unless field_15 == 1 + errors.add(:field_15, I18n.t("validations.declaration.missing"), category: :setup) + end + end + + def validate_valid_radio_option + log.attributes.each do |question_id, _v| + question = log.form.get_question(question_id, log) + + next unless question&.type == "radio" + next if log[question_id].blank? || question.answer_options.key?(log[question_id].to_s) || !question.page.routed_to?(log, nil) + + fields = field_mapping_for_errors[question_id.to_sym] || [] + + fields.each do |field| + if setup_question?(question) + errors.add(field, I18n.t("validations.invalid_option", question: QUESTIONS[field]), category: :setup) + else + errors.add(field, I18n.t("validations.invalid_option", question: QUESTIONS[field])) + end + end + end + end + + def validate_created_by_exists + return if field_3.blank? + + unless created_by + errors.add(:field_3, "User with the specified email could not be found") + end + end + + def validate_created_by_related + return unless created_by + return if created_by.organisation == owning_organisation || created_by.organisation == managing_organisation + return if created_by.organisation == owning_organisation&.absorbing_organisation || created_by.organisation == managing_organisation&.absorbing_organisation + + block_log_creation! + errors.add(:field_3, "User must be related to owning organisation or managing organisation") + end + + def created_by + @created_by ||= User.where("lower(email) = ?", field_3&.downcase).first + end + + def validate_uprn_exists_if_any_key_address_fields_are_blank + if field_16.blank? && (field_17.blank? || field_19.blank?) + errors.add(:field_16, I18n.t("validations.not_answered", question: "UPRN")) + end + end + + def validate_incomplete_soft_validations + routed_to_soft_validation_questions = log.form.questions.filter { |q| q.type == "interruption_screen" && q.page.routed_to?(log, nil) }.compact + routed_to_soft_validation_questions.each do |question| + next if question.completed?(log) + + question.page.interruption_screen_question_ids.each do |interruption_screen_question_id| + next if log.form.questions.none? { |q| q.id == interruption_screen_question_id && q.page.routed_to?(log, nil) } + + field_mapping_for_errors[interruption_screen_question_id.to_sym]&.each do |field| + if errors.none? { |e| field_mapping_for_errors[interruption_screen_question_id.to_sym].include?(e.attribute) } + error_message = [display_title_text(question.page.title_text, log), display_informative_text(question.page.informative_text, log)].reject(&:empty?).join(". ") + errors.add(field, message: error_message, category: :soft_validation) + end + end + end + end + end + + def duplicate_check_fields + [ + "startdate", + "age1", + "sex1", + "ecstat1", + "owning_organisation", + "tcharge", + !supported_housing? ? "postcode_full" : nil, + !general_needs? ? "location" : nil, + "tenancycode", + log.chcharge.present? ? "chcharge" : nil, + ].compact + end + + def validate_needs_type_present + if field_4.blank? + errors.add(:field_4, I18n.t("validations.not_answered", question: "needs type"), category: :setup) + end + end + + def start_date + return if field_8.blank? || field_9.blank? || field_10.blank? + + Date.parse("20#{field_10.to_s.rjust(2, '0')}-#{field_9}-#{field_8}") + rescue StandardError + nil + end + + def validate_no_and_dont_know_disabled_needs_conjunction + if field_83 == 1 && field_84 == 1 + errors.add(:field_83, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction")) + errors.add(:field_84, I18n.t("validations.household.housingneeds.no_and_dont_know_disabled_needs_conjunction")) + end + end + + def validate_dont_know_disabled_needs_conjunction + if field_84 == 1 && [field_79, field_80, field_81, field_82].count(1).positive? + %i[field_84 field_79 field_80 field_81 field_82].each do |field| + errors.add(field, I18n.t("validations.household.housingneeds.dont_know_disabled_needs_conjunction")) if send(field) == 1 + end + end + end + + def validate_no_disabled_needs_conjunction + if field_83 == 1 && [field_79, field_80, field_81, field_82].count(1).positive? + %i[field_83 field_79 field_80 field_81 field_82].each do |field| + errors.add(field, I18n.t("validations.household.housingneeds.no_disabled_needs_conjunction")) if send(field) == 1 + end + end + end + + def validate_only_one_housing_needs_type + if [field_79, field_80, field_81].count(1) > 1 + %i[field_79 field_80 field_81].each do |field| + errors.add(field, I18n.t("validations.household.housingneeds_type.only_one_option_permitted")) if send(field) == 1 + end + end + end + + def validate_no_housing_needs_questions_answered + if [field_79, field_80, field_81, field_82, field_83, field_84].all?(&:blank?) + errors.add(:field_83, I18n.t("validations.not_answered", question: "anybody with disabled access needs")) + errors.add(:field_82, I18n.t("validations.not_answered", question: "other access needs")) + %i[field_79 field_80 field_81].each do |field| + errors.add(field, I18n.t("validations.not_answered", question: "disabled access needs type")) + end + end + end + + def validate_reasonable_preference_homeless + reason_fields = %i[field_107 field_108 field_109 field_110 field_111] + if field_106 == 1 && reason_fields.all? { |field| attributes[field.to_s].blank? } + reason_fields.each do |field| + errors.add(field, I18n.t("validations.not_answered", question: "reason for reasonable preference")) + end + end + end + + def validate_condition_effects + illness_option_fields = %i[field_94 field_88 field_91 field_86 field_87 field_89 field_90 field_93 field_92 field_95] + if household_no_illness? + illness_option_fields.each do |field| + if attributes[field.to_s] == 1 + errors.add(field, I18n.t("validations.household.condition_effects.no_choices")) + end + end + elsif illness_option_fields.all? { |field| attributes[field.to_s].blank? } + illness_option_fields.each do |field| + errors.add(field, I18n.t("validations.not_answered", question: "how is person affected by condition or illness")) + end + end + end + + def validate_lettings_allocation + if cbl.blank? && cap.blank? && chr.blank? + errors.add(:field_112, I18n.t("validations.not_answered", question: "was the letting made under the Choice-Based Lettings (CBL)?")) + errors.add(:field_113, I18n.t("validations.not_answered", question: "was the letting made under the Common Allocation Policy (CAP)?")) + errors.add(:field_114, I18n.t("validations.not_answered", question: "was the letting made under the Common Housing Register (CHR)?")) + end + end + + def household_no_illness? + field_85 != 1 + end + + def validate_leaving_reason_for_renewal + if field_7 == 1 && ![40, 42].include?(field_98) + errors.add(:field_98, I18n.t("validations.household.reason.renewal_reason_needed")) + end + end + + def general_needs? + field_4 == 1 + end + + def supported_housing? + field_4 == 2 + end + + def validate_cannot_be_la_referral_if_general_needs_and_la + if field_116 == 4 && general_needs? && owning_organisation && owning_organisation.la? + errors.add :field_116, I18n.t("validations.household.referral.la_general_needs.prp_referred_by_la") + end + end + + def validate_la_with_local_housing_referral + if field_116 == 3 && owning_organisation && owning_organisation.la? + errors.add(:field_116, I18n.t("validations.household.referral.nominated_by_local_ha_but_la")) + end + end + + def validate_relevant_collection_window + return if start_date.blank? || bulk_upload.form.blank? + + unless bulk_upload.form.valid_start_date_for_form?(start_date) + errors.add(:field_8, I18n.t("validations.date.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + errors.add(:field_9, I18n.t("validations.date.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + errors.add(:field_10, I18n.t("validations.date.outside_collection_window", year_combo: bulk_upload.year_combo, start_year: bulk_upload.year, end_year: bulk_upload.end_year), category: :setup) + end + end + + def validate_data_types + unless attribute_set["field_11"].value_before_type_cast&.match?(/^\d+\.?0*$/) + errors.add(:field_11, I18n.t("validations.invalid_number", question: "rent type")) + end + end + + def validate_nulls + field_mapping_for_errors.each do |error_key, fields| + question_id = error_key.to_s + question = questions.find { |q| q.id == question_id } + + next unless question + next if log.optional_fields.include?(question.id) + next if question.completed?(log) + + if setup_question?(question) + fields.each do |field| + if errors.select { |e| fields.include?(e.attribute) }.none? + question_text = question.error_display_label.presence || "this question" + errors.add(field, I18n.t("validations.not_answered", question: question_text.downcase), category: :setup) if field.present? + end + end + else + fields.each do |field| + unless errors.any? { |e| fields.include?(e.attribute) } + question_text = question.error_display_label.presence || "this question" + errors.add(field, I18n.t("validations.not_answered", question: question_text.downcase)) + end + end + end + end + end + + def validate_related_location_exists + if scheme && field_6.present? && location.nil? && :field_6.present? + block_log_creation! + errors.add(:field_6, "Location code must relate to a location that is owned by the owning organisation or managing organisation", category: :setup) + end + end + + def validate_related_scheme_exists + if field_5.present? && :field_5.present? && owning_organisation.present? && managing_organisation.present? && scheme.nil? + block_log_creation! + errors.add(:field_5, "This scheme code does not belong to the owning organisation or managing organisation", category: :setup) + end + end + + def validate_managing_org_related + if owning_organisation && managing_organisation && !owning_organisation.can_be_managed_by?(organisation: managing_organisation) + block_log_creation! + + if errors[:field_2].blank? + errors.add(:field_2, "This managing organisation does not have a relationship with the owning organisation", category: :setup) + end + end + end + + def validate_managing_org_exists + if managing_organisation.nil? + block_log_creation! + + if errors[:field_2].blank? + errors.add(:field_2, "The managing organisation code is incorrect", category: :setup) + end + end + end + + def validate_managing_org_data_given + if field_2.blank? + block_log_creation! + errors.add(:field_2, "The managing organisation code is incorrect", category: :setup) + end + end + + def validate_owning_org_owns_stock + if owning_organisation && !owning_organisation.holds_own_stock? + block_log_creation! + + if errors[:field_1].blank? + errors.add(:field_1, "The owning organisation code provided is for an organisation that does not own stock", category: :setup) + end + end + end + + def validate_owning_org_exists + if owning_organisation.nil? + block_log_creation! + + if errors[:field_1].blank? + errors.add(:field_1, "The owning organisation code is incorrect", category: :setup) + end + end + end + + def validate_owning_org_data_given + if field_1.blank? + block_log_creation! + errors.add(:field_1, I18n.t("validations.not_answered", question: "owning organisation"), category: :setup) + end + end + + def validate_owning_org_permitted + if owning_organisation && !bulk_upload.user.organisation.affiliated_stock_owners.include?(owning_organisation) + block_log_creation! + + if errors[:field_1].blank? + errors.add(:field_1, "You do not have permission to add logs for this owning organisation", category: :setup) + end + end + end + + def setup_question?(question) + log.form.setup_sections[0].subsections[0].questions.include?(question) + end + + def validate_if_log_already_exists + if log_already_exists? + error_message = "This is a duplicate log" + + errors.add(:field_1, error_message) # owning_organisation + errors.add(:field_8, error_message) # startdate + errors.add(:field_9, error_message) # startdate + errors.add(:field_10, error_message) # startdate + errors.add(:field_13, error_message) # tenancycode + errors.add(:field_6, error_message) if !general_needs? && :field_6.present? # location + errors.add(:field_5, error_message) if !general_needs? && :field_6.blank? # add to Scheme field as unclear whether log uses New or Old CORE ids + errors.add(:field_21, error_message) unless supported_housing? # postcode_full + errors.add(:field_22, error_message) unless supported_housing? # postcode_full + errors.add(:field_23, error_message) unless supported_housing? # la + errors.add(:field_42, error_message) # age1 + errors.add(:field_43, error_message) # sex1 + errors.add(:field_46, error_message) # ecstat1 + errors.add(:field_124, error_message) if log.chcharge.present? # chcharge + errors.add(:field_122, error_message) unless general_needs? # household_charge + errors.add(:field_125, error_message) # brent + errors.add(:field_126, error_message) # scharge + errors.add(:field_127, error_message) # pscharge + errors.add(:field_128, error_message) # chcharge + end + end + + def field_mapping_for_errors + { + lettype: [:field_11], + tenancycode: [:field_13], + postcode_known: %i[field_23 field_21 field_22], + postcode_full: %i[field_23 field_21 field_22], + la: %i[field_23], + owning_organisation: [:field_1], + managing_organisation: [:field_2], + owning_organisation_id: [:field_1], + managing_organisation_id: [:field_2], + renewal: [:field_7], + scheme_id: (:field_5.present? ? [:field_5] : nil), + scheme: (:field_5.present? ? [:field_5] : nil), + location_id: (:field_6.present? ? [:field_6] : nil), + location: (:field_6.present? ? [:field_6] : nil), + created_by: [:field_3], + needstype: [:field_4], + rent_type: %i[field_11], + startdate: %i[field_8 field_9 field_10], + unittype_gn: %i[field_26], + builtype: %i[field_27], + wchair: %i[field_28], + beds: %i[field_29], + joint: %i[field_36], + startertenancy: %i[field_37], + tenancy: %i[field_38], + tenancyother: %i[field_39], + tenancylength: %i[field_40], + declaration: %i[field_15], + + age1_known: %i[field_42], + age1: %i[field_42], + age2_known: %i[field_48], + age2: %i[field_48], + age3_known: %i[field_52], + age3: %i[field_52], + age4_known: %i[field_56], + age4: %i[field_56], + age5_known: %i[field_60], + age5: %i[field_60], + age6_known: %i[field_64], + age6: %i[field_64], + age7_known: %i[field_68], + age7: %i[field_68], + age8_known: %i[field_72], + age8: %i[field_72], + + sex1: %i[field_43], + sex2: %i[field_49], + sex3: %i[field_53], + sex4: %i[field_57], + sex5: %i[field_61], + sex6: %i[field_65], + sex7: %i[field_69], + sex8: %i[field_73], + + ethnic_group: %i[field_44], + ethnic: %i[field_44], + national: %i[field_45], + + relat2: %i[field_47], + relat3: %i[field_51], + relat4: %i[field_55], + relat5: %i[field_59], + relat6: %i[field_63], + relat7: %i[field_67], + relat8: %i[field_71], + + ecstat1: %i[field_46], + ecstat2: %i[field_50], + ecstat3: %i[field_54], + ecstat4: %i[field_58], + ecstat5: %i[field_62], + ecstat6: %i[field_66], + ecstat7: %i[field_70], + ecstat8: %i[field_74], + + armedforces: %i[field_75], + leftreg: %i[field_76], + reservist: %i[field_77], + preg_occ: %i[field_78], + housingneeds: %i[field_78], + + illness: %i[field_85], + + layear: %i[field_96], + waityear: %i[field_97], + reason: %i[field_98], + reasonother: %i[field_99], + prevten: %i[field_100], + homeless: %i[field_101], + + prevloc: %i[field_105], + previous_la_known: %i[field_105], + ppcodenk: %i[field_102], + ppostcode_full: %i[field_103 field_104], + + reasonpref: %i[field_106], + rp_homeless: %i[field_107], + rp_insan_unsat: %i[field_108], + rp_medwel: %i[field_109], + rp_hardship: %i[field_110], + rp_dontknow: %i[field_111], + + cbl: %i[field_112], + chr: %i[field_114], + cap: %i[field_113], + letting_allocation: %i[field_112 field_113 field_114], + + referral: %i[field_116], + + net_income_known: %i[field_117], + earnings: %i[field_118], + incfreq: %i[field_119], + hb: %i[field_120], + benefits: %i[field_121], + + period: %i[field_123], + brent: %i[field_125], + scharge: %i[field_126], + pscharge: %i[field_127], + supcharg: %i[field_128], + chcharge: %i[field_124], + tcharge: %i[field_125 field_126 field_127 field_128], + household_charge: %i[field_122], + hbrentshortfall: %i[field_129], + tshortfall: %i[field_130], + + unitletas: %i[field_25], + rsnvac: %i[field_24], + sheltered: %i[field_41], + + illness_type_1: %i[field_94], + illness_type_2: %i[field_88], + illness_type_3: %i[field_91], + illness_type_4: %i[field_86], + illness_type_5: %i[field_87], + illness_type_6: %i[field_89], + illness_type_7: %i[field_90], + illness_type_8: %i[field_93], + illness_type_9: %i[field_92], + illness_type_10: %i[field_95], + + irproduct_other: %i[field_12], + + propcode: %i[field_14], + + majorrepairs: %i[field_33 field_34 field_35], + mrcdate: %i[field_33 field_34 field_35], + + voiddate: %i[field_30 field_31 field_32], + + uprn: [:field_16], + address_line1: [:field_17], + address_line2: [:field_18], + town_or_city: [:field_19], + county: [:field_20], + }.compact + end + + def attribute_set + @attribute_set ||= instance_variable_get(:@attributes) + end + + def questions + @questions ||= log.form.subsections.flat_map { |ss| ss.applicable_questions(log) } + end + + def attributes_for_log + attributes = {} + + attributes["lettype"] = nil # should get this from rent_type + attributes["tenancycode"] = field_13 + attributes["la"] = field_23 + attributes["postcode_known"] = postcode_known + attributes["postcode_full"] = postcode_full + attributes["owning_organisation"] = owning_organisation + attributes["managing_organisation"] = managing_organisation + attributes["renewal"] = renewal + attributes["scheme"] = scheme + attributes["location"] = location + attributes["created_by"] = created_by || bulk_upload.user + attributes["needstype"] = field_4 + attributes["rent_type"] = field_11 + attributes["startdate"] = startdate + attributes["unittype_gn"] = field_26 + attributes["builtype"] = field_27 + attributes["wchair"] = field_28 + attributes["beds"] = field_29 + attributes["joint"] = field_36 + attributes["startertenancy"] = field_37 + attributes["tenancy"] = field_38 + attributes["tenancyother"] = field_39 + attributes["tenancylength"] = field_40 + attributes["declaration"] = field_15 + + attributes["age1_known"] = age1_known? + attributes["age1"] = field_42 if attributes["age1_known"]&.zero? && field_42&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age2_known"] = age2_known? + attributes["age2"] = field_48 if attributes["age2_known"]&.zero? && field_48&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age3_known"] = age3_known? + attributes["age3"] = field_52 if attributes["age3_known"]&.zero? && field_52&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age4_known"] = age4_known? + attributes["age4"] = field_56 if attributes["age4_known"]&.zero? && field_56&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age5_known"] = age5_known? + attributes["age5"] = field_60 if attributes["age5_known"]&.zero? && field_60&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age6_known"] = age6_known? + attributes["age6"] = field_64 if attributes["age6_known"]&.zero? && field_64&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age7_known"] = age7_known? + attributes["age7"] = field_68 if attributes["age7_known"]&.zero? && field_68&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["age8_known"] = age8_known? + attributes["age8"] = field_72 if attributes["age8_known"]&.zero? && field_72&.match(/\A\d{1,3}\z|\AR\z/) + + attributes["sex1"] = field_43 + attributes["sex2"] = field_49 + attributes["sex3"] = field_53 + attributes["sex4"] = field_57 + attributes["sex5"] = field_61 + attributes["sex6"] = field_65 + attributes["sex7"] = field_69 + attributes["sex8"] = field_73 + + attributes["ethnic_group"] = ethnic_group_from_ethnic + attributes["ethnic"] = field_44 + attributes["national"] = field_45 + + attributes["relat2"] = field_47 + attributes["relat3"] = field_51 + attributes["relat4"] = field_55 + attributes["relat5"] = field_59 + attributes["relat6"] = field_63 + attributes["relat7"] = field_67 + attributes["relat8"] = field_71 + + attributes["ecstat1"] = field_46 + attributes["ecstat2"] = field_50 + attributes["ecstat3"] = field_54 + attributes["ecstat4"] = field_58 + attributes["ecstat5"] = field_62 + attributes["ecstat6"] = field_66 + attributes["ecstat7"] = field_70 + attributes["ecstat8"] = field_74 + + attributes["details_known_2"] = details_known?(2) + attributes["details_known_3"] = details_known?(3) + attributes["details_known_4"] = details_known?(4) + attributes["details_known_5"] = details_known?(5) + attributes["details_known_6"] = details_known?(6) + attributes["details_known_7"] = details_known?(7) + attributes["details_known_8"] = details_known?(8) + + attributes["armedforces"] = field_75 + attributes["leftreg"] = leftreg + attributes["reservist"] = field_77 + + attributes["preg_occ"] = field_78 + + attributes["housingneeds"] = housingneeds + attributes["housingneeds_type"] = housingneeds_type + attributes["housingneeds_other"] = housingneeds_other + + attributes["illness"] = field_85 + + attributes["layear"] = field_96 + attributes["waityear"] = field_97 + attributes["reason"] = field_98 + attributes["reasonother"] = field_99 + attributes["prevten"] = field_100 + attributes["homeless"] = field_101 + + attributes["prevloc"] = prevloc + attributes["previous_la_known"] = previous_la_known + attributes["ppcodenk"] = ppcodenk + attributes["ppostcode_full"] = ppostcode_full + + attributes["reasonpref"] = field_106 + attributes["rp_homeless"] = field_107 + attributes["rp_insan_unsat"] = field_108 + attributes["rp_medwel"] = field_109 + attributes["rp_hardship"] = field_110 + attributes["rp_dontknow"] = field_111 + + attributes["cbl"] = cbl + attributes["chr"] = chr + attributes["cap"] = cap + attributes["letting_allocation_unknown"] = letting_allocation_unknown + + attributes["referral"] = field_116 + + attributes["net_income_known"] = net_income_known + attributes["earnings"] = earnings + attributes["incfreq"] = field_119 + attributes["hb"] = field_120 + attributes["benefits"] = field_121 + + attributes["period"] = field_123 + attributes["brent"] = field_125 + attributes["scharge"] = field_126 + attributes["pscharge"] = field_127 + attributes["supcharg"] = field_128 + attributes["chcharge"] = field_124 + attributes["is_carehome"] = field_124.present? ? 1 : 0 + attributes["household_charge"] = supported_housing? ? field_122 : nil + attributes["hbrentshortfall"] = field_129 + attributes["tshortfall_known"] = tshortfall_known + attributes["tshortfall"] = field_130 + + attributes["hhmemb"] = hhmemb + + attributes["unitletas"] = field_25 + attributes["rsnvac"] = rsnvac + attributes["sheltered"] = field_41 + + attributes["illness_type_1"] = field_94 + attributes["illness_type_2"] = field_88 + attributes["illness_type_3"] = field_91 + attributes["illness_type_4"] = field_86 + attributes["illness_type_5"] = field_87 + attributes["illness_type_6"] = field_89 + attributes["illness_type_7"] = field_90 + attributes["illness_type_8"] = field_93 + attributes["illness_type_9"] = field_92 + attributes["illness_type_10"] = field_95 + + attributes["irproduct_other"] = field_12 + + attributes["propcode"] = field_14 + + attributes["majorrepairs"] = majorrepairs + + attributes["mrcdate"] = mrcdate + + attributes["voiddate"] = voiddate + + attributes["first_time_property_let_as_social_housing"] = first_time_property_let_as_social_housing + + attributes["uprn_known"] = field_16.present? ? 1 : 0 + attributes["uprn_confirmed"] = 1 if field_16.present? + attributes["skip_update_uprn_confirmed"] = true + attributes["uprn"] = field_16 + attributes["address_line1"] = field_17 + attributes["address_line2"] = field_18 + attributes["town_or_city"] = field_19 + attributes["county"] = field_20 + + attributes + end + + def postcode_known + if postcode_full.present? + 1 + elsif field_23.present? + 0 + end + end + + def postcode_full + "#{field_21} #{field_22}" if field_21 && field_22 + end + + def owning_organisation + Organisation.find_by_id_on_multiple_fields(field_1) + end + + def managing_organisation + Organisation.find_by_id_on_multiple_fields(field_2) + end + + def renewal + case field_7 + when 1 + 1 + when 2 + 0 + else + field_7 + end + end + + def rsnvac + field_24 + end + + def scheme + return if field_5.nil? || owning_organisation.nil? || managing_organisation.nil? + + @scheme ||= Scheme.where(id: (owning_organisation.owned_schemes + managing_organisation.owned_schemes).map(&:id)).find_by_id_on_multiple_fields(field_5, field_6) + end + + def location + return if scheme.nil? + + @location ||= scheme.locations.find_by_id_on_multiple_fields(field_6) + end + + def startdate + Date.new(field_10 + 2000, field_9, field_8) if field_10.present? && field_9.present? && field_8.present? + rescue Date::Error + Date.new + end + + def ethnic_group_from_ethnic + return nil if field_44.blank? + + case field_44 + when 1, 2, 3, 18 + 0 + when 4, 5, 6, 7 + 1 + when 8, 9, 10, 11, 15 + 2 + when 12, 13, 14 + 3 + when 16, 19 + 4 + when 17 + 17 + end + end + + def age1_known? + return 1 if field_42 == "R" + + 0 + end + + [ + { person: 2, field: :field_48 }, + { person: 3, field: :field_52 }, + { person: 4, field: :field_56 }, + { person: 5, field: :field_60 }, + { person: 6, field: :field_64 }, + { person: 7, field: :field_68 }, + { person: 8, field: :field_72 }, + ].each do |hash| + define_method("age#{hash[:person]}_known?") do + return 1 if public_send(hash[:field]) == "R" + return 0 if send("person_#{hash[:person]}_present?") + end + end + + def details_known?(person_n) + send("person_#{person_n}_present?") ? 0 : 1 + end + + def person_2_present? + field_47.present? || field_48.present? || field_49.present? + end + + def person_3_present? + field_51.present? || field_52.present? || field_53.present? + end + + def person_4_present? + field_55.present? || field_56.present? || field_57.present? + end + + def person_5_present? + field_59.present? || field_60.present? || field_61.present? + end + + def person_6_present? + field_63.present? || field_64.present? || field_65.present? + end + + def person_7_present? + field_67.present? || field_68.present? || field_69.present? + end + + def person_8_present? + field_71.present? || field_72.present? || field_73.present? + end + + def leftreg + field_76 + end + + def housingneeds + if field_83 == 1 + 2 + elsif field_84 == 1 + 3 + elsif field_83.blank? || field_83&.zero? + 1 + end + end + + def housingneeds_type + if field_79 == 1 + 0 + elsif field_80 == 1 + 1 + elsif field_81 == 1 + 2 + else + 3 + end + end + + def housingneeds_other + return 1 if field_82 == 1 + return 0 if [field_79, field_80, field_81].include?(1) + end + + def prevloc + field_105 + end + + def previous_la_known + prevloc.present? ? 1 : 0 + end + + def ppcodenk + case field_102 + when 1 + 0 + when 2 + 1 + end + end + + def ppostcode_full + "#{field_103} #{field_104}".strip.gsub(/\s+/, " ") + end + + def cbl + case field_112 + when 2 + 0 + when 1 + 1 + end + end + + def chr + case field_114 + when 2 + 0 + when 1 + 1 + end + end + + def cap + case field_113 + when 2 + 0 + when 1 + 1 + end + end + + def letting_allocation_unknown + [cbl, chr, cap].all?(0) ? 1 : 0 + end + + def net_income_known + case field_117 + when 1 + 0 + when 2 + 1 + when 3 + 2 + end + end + + def earnings + field_118.round if field_118.present? + end + + def tshortfall_known + field_129 == 1 ? 0 : 1 + end + + def hhmemb + [ + person_2_present?, + person_3_present?, + person_4_present?, + person_5_present?, + person_6_present?, + person_7_present?, + person_8_present?, + ].count(true) + 1 + end + + def majorrepairs + mrcdate.present? ? 1 : 0 + end + + def mrcdate + Date.new(field_35 + 2000, field_34, field_33) if field_35.present? && field_34.present? && field_33.present? + rescue Date::Error + Date.new + end + + def voiddate + Date.new(field_32 + 2000, field_31, field_30) if field_32.present? && field_31.present? && field_30.present? + rescue Date::Error + Date.new + end + + def first_time_property_let_as_social_housing + case rsnvac + when 15, 16, 17 + 1 + else + 0 + end + end +end diff --git a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb new file mode 100644 index 000000000..618c2ab4c --- /dev/null +++ b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2024.html.erb @@ -0,0 +1,50 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: @form.back_path %> +<% end %> + +
+
+ <%= form_with model: @form, scope: :form, url: bulk_upload_lettings_log_path(id: "prepare-your-file"), method: :patch do |f| %> + <%= f.hidden_field :year %> + + Upload lettings logs in bulk (<%= @form.year_combo %>) +

Prepare your file

+

<%= govuk_link_to "Read the full guidance", bulk_upload_lettings_log_path(id: "guidance", form: { year: @form.year }) %> before you start if you have not used bulk upload before.

+ +

Download template

+ +

Use one of these templates to upload logs for 2024/25:

+ +

There are 7 or 8 rows of content in the templates. These rows are called the ‘headers’. They contain the CORE form questions and guidance about which questions are required and how to format your answers.

+ +

Create your file

+ + + + <%= govuk_inset_text(text: "You can upload both general needs and supported housing logs in the same file for 2023/24 data.") %> + +

Save your file

+ + + + <%= f.govuk_submit class: "govuk-!-margin-top-7" %> + <% end %> +
+
diff --git a/spec/services/bulk_upload/lettings/validator_spec.rb b/spec/services/bulk_upload/lettings/validator_spec.rb index a82b7d74c..5bf741340 100644 --- a/spec/services/bulk_upload/lettings/validator_spec.rb +++ b/spec/services/bulk_upload/lettings/validator_spec.rb @@ -114,7 +114,7 @@ RSpec.describe BulkUpload::Lettings::Validator do context "when uploading a 2022 template for 2023 bulk upload" do let(:bulk_upload) { create(:bulk_upload, user:, year: 2023) } - let(:log) { build(:lettings_log, :completed, startdate: Time.zone.local(2022, 5, 6)) } + let(:log) { build(:lettings_log, :completed, startdate: Time.zone.local(2022, 5, 6), tenancycode: "5") } context "with no headers" do before do diff --git a/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb b/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb new file mode 100644 index 000000000..6db8e1806 --- /dev/null +++ b/spec/services/bulk_upload/lettings/year2024/csv_parser_spec.rb @@ -0,0 +1,226 @@ +require "rails_helper" + +RSpec.describe BulkUpload::Lettings::Year2024::CsvParser do + subject(:service) { described_class.new(path:) } + + let(:file) { Tempfile.new } + let(:path) { file.path } + let(:log) { build(:lettings_log, :completed) } + + context "when parsing csv with headers" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with headers with extra rows" do + before do + file.write("Section\n") + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(8) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with headers in arbitrary order" do + let(:seed) { rand } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row(seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row(seed:)) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(7) + expect(service.col_offset).to eq(1) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with extra invalid headers" do + let(:seed) { rand } + let(:log_to_csv) { BulkUpload::LettingsLogToCsv.new(log:) } + let(:field_numbers) { log_to_csv.default_2024_field_numbers + %w[invalid_field_number] } + let(:field_values) { log_to_csv.to_2024_row + %w[value_for_invalid_field_number] } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(log_to_csv.custom_field_numbers_row(seed:, field_numbers:)) + file.write(log_to_csv.to_custom_csv_row(seed:, field_values:)) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + + it "counts the number of valid field numbers correctly" do + expect(service).to be_correct_field_count + end + end + + context "when parsing csv without headers" do + before do + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row) + file.rewind + end + + it "returns correct offsets" do + expect(service.row_offset).to eq(0) + expect(service.col_offset).to eq(0) + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing with BOM aka byte order mark" do + let(:bom) { "\uFEFF" } + + before do + file.write(bom) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when an invalid byte sequence" do + let(:invalid_sequence) { "\x81" } + + before do + file.write(invalid_sequence) + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + context "when parsing csv with carriage returns" do + before do + file.write("Question\r\n") + file.write("Additional info\r") + file.write("Values\r\n") + file.write("Can be empty?\r") + file.write("Type of letting the question applies to\r\n") + file.write("Duplicate check field?\r") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.rewind + end + + it "parses csv correctly" do + expect(service.row_parsers[0].field_13).to eql(log.tenancycode) + end + end + + describe "#column_for_field", aggregate_failures: true do + context "when with headers using default ordering" do + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("F") + expect(service.column_for_field("field_22")).to eql("W") + end + end + + context "when without headers using default ordering" do + before do + file.write(BulkUpload::LettingsLogToCsv.new(log:, col_offset: 0).to_2024_csv_row) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("E") + expect(service.column_for_field("field_22")).to eql("V") + end + end + + context "when with headers using custom ordering" do + let(:seed) { 123 } + + before do + file.write("Question\n") + file.write("Additional info\n") + file.write("Values\n") + file.write("Can be empty?\n") + file.write("Type of letting the question applies to\n") + file.write("Duplicate check field?\n") + file.write(BulkUpload::LettingsLogToCsv.new(log:).default_2024_field_numbers_row(seed:)) + file.write(BulkUpload::LettingsLogToCsv.new(log:).to_2024_csv_row(seed:)) + file.rewind + end + + it "returns correct column" do + expect(service.column_for_field("field_5")).to eql("B") + expect(service.column_for_field("field_22")).to eql("AS") + expect(service.column_for_field("field_26")).to eql("DH") + expect(service.column_for_field("field_25")).to eql("I") + end + end + end +end diff --git a/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb new file mode 100644 index 000000000..5f34ec80a --- /dev/null +++ b/spec/services/bulk_upload/lettings/year2024/row_parser_spec.rb @@ -0,0 +1,2405 @@ +require "rails_helper" + +RSpec.describe BulkUpload::Lettings::Year2024::RowParser do + subject(:parser) { described_class.new(attributes) } + + let(:now) { Time.zone.local(2024, 4, 5) } + + let(:attributes) { { bulk_upload: } } + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: nil, year: 2024) } + let(:user) { create(:user, organisation: owning_org) } + + let(:owning_org) { create(:organisation, :with_old_visible_id) } + let(:managing_org) { create(:organisation, :with_old_visible_id) } + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let(:location) { create(:location, :with_old_visible_id, scheme:) } + + let(:setup_section_params) do + { + bulk_upload:, + field_1: owning_org.old_visible_id, + field_2: managing_org.old_visible_id, + field_4: "1", + field_7: "2", + field_8: now.day.to_s, + field_9: now.month.to_s, + field_10: now.strftime("%g"), + field_11: "0", + field_15: "1", + } + end + + before do + allow(FormHandler.instance).to receive(:lettings_in_crossover_period?).and_return(true) + create(:organisation_relationship, parent_organisation: owning_org, child_organisation: managing_org) + + LaRentRange.create!( + ranges_rent_id: "1", + la: "E09000008", + beds: 1, + lettype: 7, + soft_min: 12.41, + soft_max: 118.85, + hard_min: 9.87, + hard_max: 200.99, + start_year: 2024, + ) + end + + around do |example| + Timecop.freeze(Date.new(2024, 10, 1)) do + FormHandler.instance.use_real_forms! + example.run + end + Timecop.return + end + + describe "#blank_row?" do + context "when a new object" do + it "returns true" do + expect(parser).to be_blank_row + end + end + + context "when the only populated fields are whitespace" do + before do + parser.field_16 = " " + end + + it "returns true" do + expect(parser).to be_blank_row + end + end + + context "when any field is populated with something other than whitespace" do + before do + parser.field_1 = "1" + end + + it "returns false" do + expect(parser).not_to be_blank_row + end + end + end + + describe "validations" do + before do + stub_request(:get, /api\.postcodes\.io/) + .to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"Manchester\", \"codes\":{\"admin_district\": \"E08000003\"}}}", headers: {}) + + parser.valid? + end + + describe "#valid?" do + context "when the row is blank" do + let(:attributes) { { bulk_upload: } } + + it "returns true" do + expect(parser).to be_valid + end + end + + context "when calling the method multiple times" do + let(:attributes) { { bulk_upload:, field_130: 2 } } + + it "does not add keep adding errors to the pile" do + expect { parser.valid? }.not_to change(parser.errors, :count) + end + end + + context "when testing valid/invalid attributes" do + let(:valid_attributes) do + { + bulk_upload:, + field_13: "123", + field_8: now.day.to_s, + field_9: now.month.to_s, + field_10: now.strftime("%g"), + field_21: "EC1N", + field_22: "2TD", + field_1: owning_org.old_visible_id, + field_2: managing_org.old_visible_id, + field_11: "0", + field_7: "2", + field_26: "2", + field_27: "1", + field_28: "1", + field_29: "1", + field_36: "2", + field_37: "1", + field_38: "2", + field_15: "1", + + field_42: "42", + field_48: "41", + field_52: "20", + field_56: "18", + field_60: "16", + field_64: "14", + field_68: "12", + field_72: "20", + + field_43: "F", + field_49: "M", + field_53: "F", + field_57: "M", + field_61: "F", + field_65: "M", + field_69: "F", + field_73: "M", + + field_44: "17", + field_45: "18", + + field_47: "P", + field_51: "C", + field_55: "X", + field_59: "R", + field_63: "C", + field_67: "C", + field_71: "X", + + field_46: "1", + field_50: "2", + field_54: "6", + field_58: "7", + field_62: "8", + field_66: "9", + field_70: "0", + field_74: "10", + + field_75: "1", + field_76: "4", + field_77: "1", + + field_78: "1", + + field_79: "1", + field_80: "0", + field_81: "0", + field_82: "1", + field_83: "0", + + field_85: "2", + + field_96: "11", + field_97: "2", + field_98: "31", + field_100: "3", + field_101: "11", + + field_102: "1", + field_103: "EC1N", + field_104: "2TD", + + field_106: "1", + field_107: "1", + field_108: "", + field_109: "1", + field_110: "", + field_111: "", + + field_112: "1", + field_113: "2", + field_114: "2", + + field_116: "2", + + field_117: "1", + field_118: "2300", + field_119: "2", + field_120: "1", + field_121: "1", + + field_123: "4", + field_125: "1234.56", + field_126: "43.32", + field_127: "13.14", + field_128: "101.11", + field_129: "1", + field_130: "234.56", + + field_24: "15", + field_30: now.day.to_s, + field_31: now.month.to_s, + field_32: now.strftime("%g"), + + field_4: "1", + + field_16: "12", + } + end + + context "when valid row" do + before do + allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) + end + + let(:attributes) { valid_attributes } + + it "returns true" do + expect(parser).to be_valid + end + + xit "instantiates a log with everything completed", aggregate_failures: true do + questions = parser.send(:questions).reject do |q| + parser.send(:log).optional_fields.include?(q.id) || q.completed?(parser.send(:log)) + end + + expect(questions.map(&:id).size).to eq(0) + expect(questions.map(&:id)).to eql([]) + end + + context "when a general needs log already exists in the db" do + let(:attributes) do + valid_attributes.merge({ field_4: "1", + field_11: "0", + field_1: owning_org.old_visible_id, + field_123: 1, + field_125: 250, + field_126: 50, + field_127: 50, + field_128: 50, + field_13: "tenant_code" }) + end + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all (and only) the fields used to determine duplicates" do + parser.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_21, # postcode_full + :field_22, # postcode_full + :field_23, # postcode_full + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_125, # brent + :field_126, # scharge + :field_127, # pscharge + :field_128, # supcharg + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_6]).not_to include(error_message) + end + end + + context "when a general needs log already exists in the db but has a different tcharge" do + let(:attributes) do + valid_attributes.merge({ field_4: "1", + field_11: "0", + field_1: owning_org.old_visible_id, + field_123: 1, + field_125: 250, + field_126: 50, + field_127: 50, + field_128: 50, + field_13: "tenant_code" }) + end + + before do + parser.log.save! + saved_log = LettingsLog.find_by(tenancycode: "tenant_code") + saved_log.update!(brent: saved_log.brent + 5) + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all (and only) the fields used to determine duplicates" do + parser.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_21, # postcode_full + :field_22, # postcode_full + :field_23, # postcode_full + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_125, # brent + :field_126, # scharge + :field_127, # pscharge + :field_128, # supcharg + ].each do |field| + expect(parser.errors[field]).not_to include(error_message) + end + end + end + + context "when a supported housing log already exists in the db" do + let(:attributes) { { bulk_upload:, field_4: "2" } } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_125, # brent + :field_126, # scharge + :field_127, # pscharge + :field_128, # supcharg + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_21]).not_to include(error_message) + expect(parser.errors[:field_22]).not_to include(error_message) + expect(parser.errors[:field_23]).not_to include(error_message) + end + end + + context "with old core scheme and location ids" do + context "when a supported housing log already exists in the db" do + let(:attributes) { { bulk_upload:, field_4: "2", field_5: "123" } } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_125, # brent + :field_126, # scharge + :field_127, # pscharge + :field_128, # supcharg + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_21]).not_to include(error_message) + expect(parser.errors[:field_22]).not_to include(error_message) + expect(parser.errors[:field_23]).not_to include(error_message) + end + end + + context "when a supported housing log with chcharges already exists in the db" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: 2) } + let(:attributes) do + valid_attributes.merge({ field_5: "S#{scheme.id}", + field_4: "2", + field_11: "1", + field_6: location.old_visible_id, + field_1: owning_org.old_visible_id, + field_122: 0, + field_41: 4, + field_124: "88" }) + end + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_124, # chcharge + :field_122, # household_charge + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_21]).not_to include(error_message) + expect(parser.errors[:field_22]).not_to include(error_message) + expect(parser.errors[:field_23]).not_to include(error_message) + end + end + + context "when a supported housing log different chcharges already exists in the db" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: 2) } + let(:attributes) do + valid_attributes.merge({ field_5: "S#{scheme.id}", + field_4: "2", + field_11: "1", + field_6: location.old_visible_id, + field_1: owning_org.old_visible_id, + field_122: 0, + field_41: 4, + field_124: "88" }) + end + let(:attributes_too) do + valid_attributes.merge({ field_5: "S#{scheme.id}", + field_4: "2", + field_11: "1", + field_6: location.old_visible_id, + field_1: owning_org.old_visible_id, + field_122: 0, + field_41: 4, + field_124: "98" }) + end + let(:parser_too) { described_class.new(attributes_too) } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is a valid row" do + expect(parser_too).to be_valid + end + + it "does not add an error to all the fields used to determine duplicates" do + parser_too.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_5, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + ].each do |field| + expect(parser_too.errors[field]).not_to include(error_message) + end + end + end + end + + context "with new core scheme and location ids" do + context "when a supported housing log already exists in the db" do + let(:attributes) { { bulk_upload:, field_4: "2", field_5: "S123" } } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_21]).not_to include(error_message) + expect(parser.errors[:field_22]).not_to include(error_message) + expect(parser.errors[:field_23]).not_to include(error_message) + end + end + + context "when a supported housing log with chcharges already exists in the db" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: 2) } + let(:attributes) do + valid_attributes.merge({ field_5: "S#{scheme.id}", + field_4: "2", + field_11: "1", + field_6: location.id, + field_1: owning_org.old_visible_id, + field_122: 0, + field_41: 4, + field_124: "88" }) + end + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + + it "adds an error to all the fields used to determine duplicates" do + parser.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_124, # chcharge + :field_122, # household_charge + ].each do |field| + expect(parser.errors[field]).to include(error_message) + end + + expect(parser.errors[:field_21]).not_to include(error_message) + expect(parser.errors[:field_22]).not_to include(error_message) + expect(parser.errors[:field_23]).not_to include(error_message) + end + end + + context "when a supported housing log different chcharges already exists in the db" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: 2) } + let(:attributes) do + valid_attributes.merge({ field_5: "S#{scheme.id}", + field_4: "2", + field_11: "1", + field_6: location.id, + field_1: owning_org.old_visible_id, + field_122: 0, + field_41: 4, + field_124: "88" }) + end + let(:attributes_too) do + valid_attributes.merge({ field_5: "S#{scheme.id}", + field_4: "2", + field_11: "1", + field_6: location.id, + field_1: owning_org.old_visible_id, + field_122: 0, + field_41: 4, + field_124: "98" }) + end + let(:parser_too) { described_class.new(attributes_too) } + + before do + parser.log.save! + parser.instance_variable_set(:@valid, nil) + end + + it "is a valid row" do + expect(parser_too).to be_valid + end + + it "does not add an error to all the fields used to determine duplicates" do + parser_too.valid? + + error_message = "This is a duplicate log" + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_13, # tenancycode + :field_6, # location + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + :field_125, # brent + :field_126, # scharge + :field_127, # pscharge + :field_128, # supcharg + ].each do |field| + expect(parser_too.errors[field]).not_to include(error_message) + end + end + end + end + + context "when the rent range validation is triggered but the log has no scheme or location id" do + let(:attributes) do + setup_section_params.merge({ field_5: nil, + field_6: nil, + field_125: 300, + field_123: 1, + field_29: 1, + field_4: 1, + field_11: "1", + field_23: "E09000008" }) + end + + it "is not a valid row" do + expect(parser).not_to be_valid + end + end + + context "when a hidden log already exists in db" do + before do + parser.log.status = "pending" + parser.log.skip_update_status = true + parser.log.save! + end + + it "is a valid row" do + expect(parser).to be_valid + end + + it "does not add duplicate errors" do + parser.valid? + + [ + :field_1, # owning_organisation + :field_8, # startdate + :field_9, # startdate + :field_10, # startdate + :field_6, # location + :field_21, # postcode_full + :field_22, # postcode_full + :field_23, # postcode_full + :field_42, # age1 + :field_43, # sex1 + :field_46, # ecstat1 + ].each do |field| + expect(parser.errors[field]).to be_blank + end + end + end + end + + context "when valid row with valid decimal (integer) field_11" do + before do + allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) + end + + let(:attributes) { valid_attributes.merge(field_11: "0.00") } + + it "returns true" do + expect(parser).to be_valid + end + end + + context "when valid row with invalid decimal (non-integer) field_11" do + before do + allow(FeatureToggle).to receive(:bulk_upload_duplicate_log_check_enabled?).and_return(true) + end + + let(:attributes) { valid_attributes.merge(field_11: "0.56") } + + it "returns false" do + expect(parser).not_to be_valid + end + end + end + + describe "#validate_nulls" do + context "when non-setup questions are null" do + let(:attributes) { setup_section_params.merge({ field_16: "", field_17: "", field_19: "" }) } + + it "fetches the question's check_answer_label if it exists, otherwise it gets the question's header" do + parser.valid? + expect(parser.errors[:field_17]).to eql(["You must answer address line 1"]) + expect(parser.errors[:field_19]).to eql(["You must answer town or city"]) + end + end + end + end + + context "when setup section not complete" do + let(:attributes) { { bulk_upload:, field_13: "123" } } + + it "has errors on setup fields" do + errors = parser.errors.select { |e| e.options[:category] == :setup }.map(&:attribute).sort + + expect(errors).to eql(%i[field_1 field_10 field_11 field_15 field_2 field_4 field_7 field_8 field_9]) + end + end + + describe "#field_3" do # created_by + context "when blank" do + let(:attributes) { { bulk_upload:, field_3: "", field_4: 1 } } + + it "is permitted" do + expect(parser.errors[:field_3]).to be_blank + end + end + + context "when user could not be found" do + let(:attributes) { { bulk_upload:, field_3: "idonotexist@example.com" } } + + it "is not permitted" do + expect(parser.errors[:field_3]).to be_present + end + end + + context "when an unaffiliated user" do + let(:other_user) { create(:user) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_3: other_user.email, field_2: managing_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_3]).to be_present + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when an user part of owning org" do + let(:other_user) { create(:user, organisation: owning_org) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_3: other_user.email, field_2: managing_org.old_visible_id } } + + it "is permitted" do + expect(parser.errors[:field_3]).to be_blank + end + end + + context "when email matches other than casing" do + let(:other_user) { create(:user, organisation: owning_org) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_3: other_user.email.upcase!, field_2: managing_org.old_visible_id } } + + it "is permitted" do + expect(parser.errors[:field_3]).to be_blank + end + end + + context "when an user part of managing org" do + let(:other_user) { create(:user, organisation: managing_org) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_3: other_user.email, field_2: managing_org.old_visible_id } } + + it "is permitted" do + expect(parser.errors[:field_3]).to be_blank + end + end + end + + describe "#field_5, field_6" do # scheme and location fields + context "when nullable not permitted" do + let(:attributes) { { bulk_upload:, field_4: "2", field_11: "1", field_5: nil, field_6: nil } } + + it "cannot be nulled" do + expect(parser.errors[:field_5]).to eq(["You must answer scheme code"]) + expect(parser.errors[:field_6]).to eq(["You must answer location code"]) + end + end + + context "when nullable permitted" do + let(:attributes) { { bulk_upload:, field_4: "1", field_11: "0", field_5: nil, field_6: nil } } + + it "can be nulled" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when using New CORE ids" do + let(:scheme) { create(:scheme, :with_old_visible_id, owning_organisation: owning_org) } + let!(:location) { create(:location, :with_old_visible_id, scheme:) } + + before do + parser.valid? + end + + context "when matching scheme cannot be found" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "1", field_5: "S123", field_6: location.id } } + + it "returns a setup error" do + expect(parser.errors.where(:field_5, category: :setup).map(&:message)).to eq(["This scheme code does not belong to the owning organisation or managing organisation"]) + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when missing location" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "1", field_5: "S#{scheme.id}", field_6: nil } } + + it "returns a setup error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors.where(:field_6, category: :setup).map(&:message)).to eq(["You must answer location code"]) + expect(parser.errors[:field_6].count).to eq(1) + end + end + + context "when matching location cannot be found" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "1", field_5: "S#{scheme.id}", field_6: "123" } } + + it "returns a setup error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors.where(:field_6, category: :setup).map(&:message)).to eq(["Location code must relate to a location that is owned by the owning organisation or managing organisation"]) + end + end + + context "when matching location exists" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "1", field_5: "S#{scheme.id}", field_6: location.id } } + + it "does not return an error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when location exists but not related" do + let(:other_scheme) { create(:scheme, :with_old_visible_id) } + let(:other_location) { create(:location, :with_old_visible_id, scheme: other_scheme) } + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "1", field_5: "S#{scheme.id}", field_6: other_location.id } } + + it "returns a setup error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors.where(:field_6, category: :setup).map(&:message)).to eq(["Location code must relate to a location that is owned by the owning organisation or managing organisation"]) + end + end + + context "when scheme belongs to someone else" do + let(:other_scheme) { create(:scheme, :with_old_visible_id) } + let(:other_location) { create(:location, :with_old_visible_id, scheme: other_scheme) } + let(:attributes) { { bulk_upload:, field_4: "2", field_11: "1", field_5: "S#{other_scheme.id}", field_6: other_location.id, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "returns a setup error" do + expect(parser.errors.where(:field_5, category: :setup).map(&:message)).to eq(["This scheme code does not belong to the owning organisation or managing organisation"]) + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when scheme belongs to owning org" do + let(:attributes) { { bulk_upload:, field_4: "2", field_11: "1", field_5: "S#{scheme.id}", field_6: location.id, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "does not return an error" do + expect(parser.errors[:field_5]).to be_blank + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when scheme belongs to managing org" do + let(:managing_org_scheme) { create(:scheme, :with_old_visible_id, owning_organisation: managing_org) } + let(:managing_org_location) { create(:location, :with_old_visible_id, scheme: managing_org_scheme) } + let(:attributes) { { bulk_upload:, field_4: "2", field_11: "1", field_5: "S#{managing_org_scheme.id}", field_6: managing_org_location.id, field_2: managing_org.old_visible_id } } + + it "clears the scheme answer" do + expect(parser.errors[:field_5]).to include("You must answer scheme name") + expect(parser.errors[:field_6]).to be_blank + end + end + + context "when matching location exists but is incomplete" do + let(:incomplete_location) { create(:location, :with_old_visible_id, :incomplete, scheme:) } + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "2", field_11: "1", field_5: "S#{scheme.id}", field_6: incomplete_location.id } } + + it "returns a setup error for scheme" do + expect(parser.errors.where(:field_5).map(&:message)).to eq(["This location is incomplete. Select another location or update this one"]) + expect(parser.errors.where(:field_6).map(&:message)).to eq(["This location is incomplete. Select another location or update this one"]) + end + end + end + end + + describe "#field_98" do # leaving reason + context "when field_7 is 1 meaning it is a renewal" do + context "when field_98 is 40" do + let(:attributes) { { bulk_upload:, field_98: "40", field_7: "1" } } + + it "is permitted" do + expect(parser.errors[:field_98]).to be_blank + end + end + + context "when field_98 is 42" do + let(:attributes) { { bulk_upload:, field_98: "42", field_7: "1" } } + + it "is permitted" do + expect(parser.errors[:field_98]).to be_blank + end + end + + context "when field_98 is not 40 or 42" do + let(:attributes) { { bulk_upload:, field_98: "1", field_7: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_98]).to be_present + end + end + end + + context "when no longer a valid option from previous year" do + let(:attributes) { setup_section_params.merge({ field_98: "7" }) } + + it "returns an error" do + expect(parser.errors[:field_98]).to include("Enter a valid value for What is the tenant’s main reason for the household leaving their last settled home?") + end + end + end + + describe "#field_79, #field_80, #field_81" do + context "when one item selected" do + let(:attributes) { { bulk_upload:, field_79: "1" } } + + it "is permitted" do + expect(parser.errors[:field_79]).to be_blank + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when more than one item selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_80: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_79]).to be_present + expect(parser.errors[:field_80]).to be_present + end + end + end + + describe "#field_83" do + context "when 1 and another disability field selected" do + let(:attributes) { { bulk_upload:, field_83: "1", field_82: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_83]).to be_present + end + end + end + + describe "#field_84" do + context "when 1 and another disability field selected" do + let(:attributes) { { bulk_upload:, field_84: "1", field_82: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_84]).to be_present + end + end + end + + describe "#field_83, #field_84" do + context "when both 1" do + let(:attributes) { { bulk_upload:, field_83: "1", field_84: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_83]).to be_present + expect(parser.errors[:field_84]).to be_present + end + end + end + + describe "#field_79 - #field_84" do + context "when all blank" do + let(:attributes) { setup_section_params.merge({ field_79: nil, field_80: nil, field_81: nil, field_82: nil, field_83: nil, field_84: nil }) } + + it "adds errors to correct fields" do + expect(parser.errors[:field_79]).to be_present + expect(parser.errors[:field_80]).to be_present + expect(parser.errors[:field_81]).to be_present + expect(parser.errors[:field_82]).to be_present + expect(parser.errors[:field_83]).to be_present + end + end + + context "when one item selected and field_82 is blank" do + let(:attributes) { setup_section_params.merge({ field_79: "1", field_82: nil }) } + + it "sets other disabled access needs as no" do + expect(parser.errors[:field_79]).to be_blank + expect(parser.errors[:field_82]).to be_blank + expect(parser.log.housingneeds_other).to eq(0) + end + end + end + + describe "#field_85, field_94 - 99" do + context "when no illness but illnesses answered" do + let(:attributes) { { bulk_upload:, field_85: "2", field_86: "1", field_87: "1", field_88: "1" } } + + it "errors added to correct fields" do + expect(parser.errors[:field_86]).to be_present + expect(parser.errors[:field_87]).to be_present + expect(parser.errors[:field_88]).to be_present + expect(parser.errors[:field_89]).not_to be_present + expect(parser.errors[:field_90]).not_to be_present + expect(parser.errors[:field_91]).not_to be_present + expect(parser.errors[:field_92]).not_to be_present + expect(parser.errors[:field_93]).not_to be_present + expect(parser.errors[:field_94]).not_to be_present + expect(parser.errors[:field_95]).not_to be_present + end + end + + context "when illness but no illnesses answered" do + let(:attributes) { { bulk_upload:, field_85: "1", field_86: nil, field_87: nil, field_88: nil, field_89: nil, field_90: nil, field_91: nil, field_92: nil, field_93: nil, field_94: nil, field_95: nil } } + + it "errors added to correct fields" do + expect(parser.errors[:field_86]).to be_present + expect(parser.errors[:field_87]).to be_present + expect(parser.errors[:field_88]).to be_present + expect(parser.errors[:field_89]).to be_present + expect(parser.errors[:field_90]).to be_present + expect(parser.errors[:field_91]).to be_present + expect(parser.errors[:field_92]).to be_present + expect(parser.errors[:field_93]).to be_present + expect(parser.errors[:field_94]).to be_present + expect(parser.errors[:field_95]).to be_present + end + end + end + + describe "#field_112, 117, 118" do + context "when none of field_112, 117, 118 are given" do + let(:attributes) { { bulk_upload:, field_112: "", field_113: "", field_114: "", field_85: "1" } } + + it "sets correct errors" do + expect(parser.errors[:field_112]).to include("You must answer was the letting made under the Choice-Based Lettings (CBL)?") + expect(parser.errors[:field_113]).to include("You must answer was the letting made under the Common Allocation Policy (CAP)?") + expect(parser.errors[:field_114]).to include("You must answer was the letting made under the Common Housing Register (CHR)?") + end + end + end + + describe "#field_101, field_106 - 15" do + context "when there is a reasonable preference but none is given" do + let(:attributes) { { bulk_upload:, field_106: "1", field_107: nil, field_108: nil, field_109: nil, field_110: nil, field_111: nil } } + + it "is not permitted" do + expect(parser.errors[:field_107]).to be_present + expect(parser.errors[:field_108]).to be_present + expect(parser.errors[:field_109]).to be_present + expect(parser.errors[:field_110]).to be_present + expect(parser.errors[:field_111]).to be_present + end + end + end + + describe "#field_116" do # referral + context "when 3 ie PRP nominated by LA and owning org is LA" do + let(:attributes) { { bulk_upload:, field_116: "3", field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "is not permitted" do + expect(parser.errors[:field_116]).to be_present + end + end + + context "when 4 ie referred by LA and is general needs and owning org is LA" do + let(:attributes) { { bulk_upload:, field_116: "4", field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_4: "1" } } + + it "is not permitted" do + expect(parser.errors[:field_116]).to be_present + end + end + + context "when 4 ie referred by LA and is general needs and owning org is PRP" do + let(:owning_org) { create(:organisation, :prp, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_116: "4", field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "is permitted" do + expect(parser.errors[:field_116]).to be_blank + end + end + + context "when 4 ie referred by LA and is not general needs" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:) } + let(:attributes) { { bulk_upload:, field_116: "4", field_4: "2" } } + + it "is permitted" do + expect(parser.errors[:field_116]).to be_blank + end + end + end + + describe "fields 7, 8, 9 => startdate" do + context "when any one of these fields is blank" do + let(:attributes) { { bulk_upload:, field_11: "0", field_8: nil, field_9: nil, field_10: nil } } + + it "returns an error" do + expect(parser.errors[:field_8]).to be_present + expect(parser.errors[:field_9]).to be_present + expect(parser.errors[:field_10]).to be_present + end + end + + context "when field_10 is 4 digits instead of 2" do + let(:attributes) { { bulk_upload:, field_10: "2023" } } + + it "returns an error" do + expect(parser.errors[:field_10]).to include("Tenancy start year must be 2 digits") + end + end + + context "when invalid date given" do + let(:attributes) { { bulk_upload:, field_11: "0", field_8: "a", field_9: "12", field_10: "23" } } + + it "does not raise an error" do + expect { parser.valid? }.not_to raise_error + end + end + + context "when inside of collection year" do + let(:attributes) { { bulk_upload:, field_8: "1", field_9: "10", field_10: "23" } } + + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year: 2022) } + + it "does not return errors" do + expect(parser.errors[:field_8]).not_to be_present + expect(parser.errors[:field_9]).not_to be_present + expect(parser.errors[:field_10]).not_to be_present + end + end + + context "when outside of collection year" do + around do |example| + Timecop.freeze(Date.new(2023, 4, 2)) do + example.run + end + end + + let(:attributes) { { bulk_upload:, field_8: "1", field_9: "1", field_10: "23" } } + + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, year: 2023) } + + it "returns setup errors" do + expect(parser.errors.where(:field_8, category: :setup)).to be_present + expect(parser.errors.where(:field_9, category: :setup)).to be_present + expect(parser.errors.where(:field_10, category: :setup)).to be_present + end + end + end + + describe "#field_1" do # owning org + context "when blank" do + let(:attributes) { { bulk_upload:, field_1: "", field_4: 1 } } + + it "is not permitted as setup error" do + expect(parser.errors.where(:field_1, category: :setup).map(&:message)).to eql(["You must answer owning organisation"]) + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when cannot find owning org" do + let(:attributes) { { bulk_upload:, field_1: "donotexist" } } + + it "is not permitted as setup error" do + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_1 }.message).to eql("The owning organisation code is incorrect") + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when org is not stock owning" do + let(:owning_org) { create(:organisation, :with_old_visible_id, :does_not_own_stock) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id } } + + it "is not permitted as setup error" do + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_1 }.message).to eql("The owning organisation code provided is for an organisation that does not own stock") + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when not affiliated with owning org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_1: unaffiliated_org.old_visible_id } } + + it "is not permitted as setup error" do + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_1 }.message).to eql("You do not have permission to add logs for this owning organisation") + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when user's org has absorbed owning organisation" do + let(:merged_org) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + let(:merged_org_stock_owner) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + + let(:attributes) { { bulk_upload:, field_1: merged_org_stock_owner.old_visible_id, field_2: merged_org.old_visible_id, field_3: user.email } } + + before do + create(:organisation_relationship, parent_organisation: merged_org_stock_owner, child_organisation: merged_org) + merged_org.update!(absorbing_organisation: user.organisation, merge_date: Time.zone.today) + merged_org.reload + user.organisation.reload + end + + it "is permitted" do + parser = described_class.new(attributes) + + parser.valid? + expect(parser.errors.where(:field_1)).not_to be_present + expect(parser.errors.where(:field_3)).not_to be_present + end + end + + context "when user's org has absorbed owning organisation before the startdate" do + let(:merged_org) { create(:organisation, :with_old_visible_id, holds_own_stock: true) } + + let(:attributes) { setup_section_params.merge({ field_1: merged_org.old_visible_id, field_2: merged_org.old_visible_id, field_3: user.email }) } + + before do + merged_org.update!(absorbing_organisation: user.organisation, merge_date: Time.zone.today - 5.years) + merged_org.reload + user.organisation.reload + end + + it "is not permitted" do + parser = described_class.new(attributes) + + parser.valid? + expect(parser.errors[:field_1]).to include(/The owning organisation must be active on the tenancy start date/) + expect(parser.errors[:field_2]).to include(/The managing organisation must be active on the tenancy start date/) + expect(parser.errors[:field_8]).to include(/Enter a date when the owning and managing organisation was active/) + expect(parser.errors[:field_9]).to include(/Enter a date when the owning and managing organisation was active/) + expect(parser.errors[:field_10]).to include(/Enter a date when the owning and managing organisation was active/) + end + end + end + + describe "#field_2" do # managing org + context "when blank" do + let(:attributes) { { bulk_upload:, field_2: "", field_4: 1 } } + + it "is not permitted as setup error" do + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_2 }.message).to eql("The managing organisation code is incorrect") + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when cannot find managing org" do + let(:attributes) { { bulk_upload:, field_2: "donotexist" } } + + it "is not permitted as setup error" do + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_2 }.message).to eql("The managing organisation code is incorrect") + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + + context "when not affiliated with managing org" do + let(:unaffiliated_org) { create(:organisation, :with_old_visible_id) } + + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: unaffiliated_org.old_visible_id } } + + it "is not permitted as setup error" do + setup_errors = parser.errors.select { |e| e.options[:category] == :setup } + + expect(setup_errors.find { |e| e.attribute == :field_2 }.message).to eql("This managing organisation does not have a relationship with the owning organisation") + end + + it "blocks log creation" do + expect(parser).to be_block_log_creation + end + end + end + + describe "#field_4" do # needs type + context "when blank" do + let(:attributes) { { bulk_upload:, field_4: nil, field_13: "123" } } + + it "is reported as a setup error" do + expect(parser.errors.where(:field_4, category: :setup).map(&:message)).to eql(["You must answer needs type"]) + end + end + end + + describe "#field_7" do # renewal + context "when blank" do + let(:attributes) { { bulk_upload:, field_1: owning_org.old_visible_id, field_2: owning_org.old_visible_id, field_7: "" } } + + it "has setup errors on the field" do + expect(parser.errors.where(:field_7, category: :setup).map(&:message)).to eql(["You must answer property renewal"]) + end + end + + context "when none possible option selected" do + let(:attributes) { setup_section_params.merge({ field_7: "101" }) } + + it "adds a setup error" do + expect(parser.errors.where(:field_7, category: :setup).map(&:message)).to include("Enter a valid value for Is this letting a renewal?") + end + end + end + + describe "#field_16" do # UPRN + context "when over 12 characters" do + let(:attributes) { setup_section_params.merge({ field_16: "1234567890123" }) } + + it "adds an appropriate error" do + expect(parser.errors[:field_16]).to eql(["UPRN is not recognised. Check the number, or enter the address"]) + end + end + + context "when neither UPRN nor address fields are given" do + let(:attributes) { setup_section_params } + + it "adds appropriate errors" do + expect(parser.errors[:field_16]).to eql(["You must answer UPRN"]) + expect(parser.errors[:field_17]).to eql(["You must answer address line 1"]) + expect(parser.errors[:field_19]).to eql(["You must answer town or city"]) + end + end + + context "when neither UPRN nor address fields are given for a supported housing record" do + let(:bulk_upload) { create(:bulk_upload, :lettings, user:, needstype: 2) } + let(:attributes) do + { bulk_upload:, + field_5: "S#{scheme.id}", + field_4: "2", + field_11: "1", + field_6: location.old_visible_id, + field_1: "1" } + end + + it "does not add UPRN errors" do + expect(parser.errors[:field_16]).to be_empty + expect(parser.errors[:field_17]).to be_empty + expect(parser.errors[:field_19]).to be_empty + end + end + + context "when UPRN is given but address fields are not" do + let(:attributes) do + { + bulk_upload:, + field_16: "123456789012", + } + end + + it "doesn't add an error" do + expect(parser.errors[:field_16]).to be_empty + end + end + + context "when address is given but UPRN is not" do + let(:attributes) do + { + bulk_upload:, + field_17: "1 Example Rd", + field_19: "Example Town/City", + } + end + + it "doesn't add an error" do + expect(parser.errors[:field_16]).to be_empty + end + end + end + + describe "#field_25" do # unitletas + context "when no longer a valid option from previous year" do + let(:attributes) { setup_section_params.merge({ field_25: "4" }) } + + it "returns an error" do + expect(parser.errors[:field_25]).to be_present + end + end + end + + describe "#field_27" do + context "when null" do + let(:attributes) { setup_section_params.merge({ field_27: nil }) } + + it "returns an error" do + expect(parser.errors[:field_27]).to be_present + end + + it "populates with correct error message" do + expect(parser.errors[:field_27]).to eql(["You must answer type of building"]) + end + end + end + + describe "#field_48" do # age2 + context "when null but gender given" do + let(:attributes) { setup_section_params.merge({ field_48: "", field_49: "F" }) } + + it "returns an error" do + expect(parser.errors[:field_48]).to be_present + end + end + end + + describe "soft validations" do + context "when soft validation is triggered" do + let(:attributes) { setup_section_params.merge({ field_42: 22, field_46: 5 }) } + + it "adds an error to the relevant fields" do + expect(parser.errors.where(:field_42, category: :soft_validation)).to be_present + expect(parser.errors.where(:field_46, category: :soft_validation)).to be_present + end + + it "populates with correct error message" do + expect(parser.errors.where(:field_42, category: :soft_validation).first.message).to eql("You told us this person is aged 22 years and retired.") + expect(parser.errors.where(:field_46, category: :soft_validation).first.message).to eql("You told us this person is aged 22 years and retired.") + end + end + + context "when a soft validation is triggered that relates both to fields that are and are not routed to" do + let(:attributes) { setup_section_params.merge({ field_78: "1", field_43: "M", field_49: "M", field_53: "M" }) } + + it "adds errors to fields that are routed to" do + expect(parser.errors.where(:field_49, category: :soft_validation)).to be_present + expect(parser.errors.where(:field_53, category: :soft_validation)).to be_present + end + + it "does not add errors to fields that are not routed to" do + expect(parser.errors.where(:field_57, category: :soft_validation)).not_to be_present + expect(parser.errors.where(:field_61, category: :soft_validation)).not_to be_present + end + end + + context "when soft validation is triggered and not required" do + let(:attributes) { setup_section_params.merge({ field_125: 120, field_123: 1, field_29: 1, field_4: 1, field_11: "1", field_23: "E09000008" }) } + + it "adds an error to the relevant fields" do + expect(parser.errors.where(:field_125, category: :soft_validation)).to be_present + end + + it "populates with correct error message" do + expect(parser.errors.where(:field_125, category: :soft_validation).count).to be(1) + expect(parser.errors.where(:field_125, category: :soft_validation).first.message).to eql("You told us the rent is £120.00 every week. This is higher than we would expect.") + end + end + end + end + + describe "#log" do + describe "#created_by" do + context "when blank" do + let(:attributes) { setup_section_params } + + it "takes the user that is uploading" do + expect(parser.log.created_by).to eql(bulk_upload.user) + end + end + + context "when email specified" do + let(:other_user) { create(:user, organisation: owning_org) } + + let(:attributes) { setup_section_params.merge(field_3: other_user.email) } + + it "sets to user with specified email" do + expect(parser.log.created_by).to eql(other_user) + end + end + end + + describe "#uprn" do + let(:attributes) { { bulk_upload:, field_16: "12" } } + + it "sets to given value" do + expect(parser.log.uprn).to eql("12") + end + end + + describe "#uprn_known" do + context "when uprn specified" do + let(:attributes) { { bulk_upload:, field_16: "12" } } + + it "sets to 1" do + expect(parser.log.uprn_known).to be(1) + expect(parser.log.uprn_confirmed).to be(1) + end + end + + context "when uprn blank" do + let(:attributes) { { bulk_upload:, field_16: "", field_4: 1 } } + + it "sets to 0" do + expect(parser.log.uprn_known).to be(0) + end + end + end + + describe "#address_line1" do + let(:attributes) { { bulk_upload:, field_17: "123 Sesame Street" } } + + it "sets to given value" do + expect(parser.log.address_line1).to eql("123 Sesame Street") + end + end + + describe "#address_line2" do + let(:attributes) { { bulk_upload:, field_18: "Cookie Town" } } + + it "sets to given value" do + expect(parser.log.address_line2).to eql("Cookie Town") + end + end + + describe "#town_or_city" do + let(:attributes) { { bulk_upload:, field_19: "London" } } + + it "sets to given value" do + expect(parser.log.town_or_city).to eql("London") + end + end + + describe "#county" do + let(:attributes) { { bulk_upload:, field_20: "Greater London" } } + + it "sets to given value" do + expect(parser.log.county).to eql("Greater London") + end + end + + [ + %w[age1_known details_known_1 age1 field_42 field_47 field_49], + %w[age2_known details_known_2 age2 field_48 field_47 field_49], + %w[age3_known details_known_3 age3 field_52 field_51 field_53], + %w[age4_known details_known_4 age4 field_56 field_55 field_57], + %w[age5_known details_known_5 age5 field_60 field_59 field_61], + %w[age6_known details_known_6 age6 field_64 field_63 field_65], + %w[age7_known details_known_7 age7 field_68 field_67 field_69], + %w[age8_known details_known_8 age8 field_72 field_71 field_73], + ].each do |known, details_known, age, field, relationship, gender| + describe "##{known} and ##{age}" do + context "when #{field} is blank" do + context "and person details are not given" do + let(:attributes) { { bulk_upload:, field.to_sym => nil, field_4: 1, relationship.to_sym => nil, gender.to_sym => nil } } + + it "does not set ##{known}" do + unless known == "age1_known" + expect(parser.log.public_send(known)).to be_nil + end + end + + it "sets ##{details_known} to no" do + unless details_known == "details_known_1" + expect(parser.log.public_send(details_known)).to eq(1) + end + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end + + context "and person details are given" do + let(:attributes) { { bulk_upload:, field.to_sym => nil, field_4: 1, relationship.to_sym => "C", gender.to_sym => "X" } } + + it "does not set ##{age}" do + parser.valid? + expect(parser.errors[field.to_sym]).to include(/must be a number or the letter R/) + end + end + end + + context "when #{field} is R" do + let(:attributes) { { bulk_upload:, field.to_s => "R" } } + + it "sets ##{known} 1" do + expect(parser.log.public_send(known)).to be(1) + end + + it "sets ##{age} to nil" do + expect(parser.log.public_send(age)).to be_nil + end + end + + context "when #{field} is a number" do + let(:attributes) { { bulk_upload:, field.to_s => "50" } } + + it "sets ##{known} to 0" do + expect(parser.log.public_send(known)).to be(0) + end + + it "sets ##{age} to given age" do + expect(parser.log.public_send(age)).to be(50) + end + end + end + end + + describe "#location" do + context "when lookup is via new core id" do + let(:attributes) { { bulk_upload:, field_5: "S#{scheme.id}", field_6: location.id, field_1: "ORG#{owning_org.id}", field_2: "ORG#{owning_org.id}" } } + + it "assigns the correct location" do + expect(parser.log.location).to eql(location) + end + end + end + + describe "#scheme" do + context "when lookup is via new core id" do + let(:attributes) { { bulk_upload:, field_5: "S#{scheme.id}", field_1: "ORG#{owning_org.id}", field_2: "ORG#{owning_org.id}" } } + + it "assigns the correct scheme" do + expect(parser.log.scheme).to eql(scheme) + end + end + end + + describe "#owning_organisation" do + context "when lookup is via id prefixed with ORG" do + let(:attributes) { { bulk_upload:, field_1: "ORG#{owning_org.id}" } } + + it "assigns the correct org" do + expect(parser.log.owning_organisation).to eql(owning_org) + end + end + end + + describe "#managing_organisation" do + context "when lookup is via id prefixed with ORG" do + let(:attributes) { { bulk_upload:, field_2: "ORG#{managing_org.id}" } } + + it "assigns the correct org" do + expect(parser.log.managing_organisation).to eql(managing_org) + end + end + end + + describe "#cbl" do + context "when field_112 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_112: 1 } } + + it "sets value to 1" do + expect(parser.log.cbl).to be(1) + end + end + + context "when field_112 is no ie 2" do + let(:attributes) { { bulk_upload:, field_112: 2 } } + + it "sets value to 0" do + expect(parser.log.cbl).to be(0) + end + end + end + + describe "#chr" do + context "when field_114 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_114: 1 } } + + it "sets value to 1" do + expect(parser.log.chr).to be(1) + end + end + + context "when field_114 is no ie 2" do + let(:attributes) { { bulk_upload:, field_114: 2 } } + + it "sets value to 0" do + expect(parser.log.chr).to be(0) + end + end + end + + describe "#cap" do + context "when field_113 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_113: 1 } } + + it "sets value to 1" do + expect(parser.log.cap).to be(1) + end + end + + context "when field_113 is no ie 2" do + let(:attributes) { { bulk_upload:, field_113: 2 } } + + it "sets value to 0" do + expect(parser.log.cap).to be(0) + end + end + end + + describe "#letting_allocation_unknown" do + context "when field_112, 117, 118 are no ie 2" do + let(:attributes) { { bulk_upload:, field_112: 2, field_113: 2, field_114: 2 } } + + it "sets value to 1" do + expect(parser.log.letting_allocation_unknown).to be(1) + end + end + + context "when any one of field_112, 117, 118 is yes ie 1" do + let(:attributes) { { bulk_upload:, field_112: 1 } } + + it "sets value to 0" do + expect(parser.log.letting_allocation_unknown).to be(0) + end + end + end + + describe "#renewal" do + context "when field_7 is no ie 2" do + let(:attributes) { { bulk_upload:, field_7: 2 } } + + it "sets value to 0" do + expect(parser.log.renewal).to eq(0) + end + end + end + + describe "#sexN fields" do + let(:attributes) do + { + bulk_upload:, + field_43: "F", + field_49: "M", + field_53: "X", + field_57: "R", + field_61: "F", + field_65: "M", + field_69: "X", + field_73: "R", + } + end + + it "sets value from correct mapping" do + expect(parser.log.sex1).to eql("F") + expect(parser.log.sex2).to eql("M") + expect(parser.log.sex3).to eql("X") + expect(parser.log.sex4).to eql("R") + expect(parser.log.sex5).to eql("F") + expect(parser.log.sex6).to eql("M") + expect(parser.log.sex7).to eql("X") + expect(parser.log.sex8).to eql("R") + end + end + + describe "#ecstatN fields" do + let(:attributes) do + { + bulk_upload:, + field_46: "1", + field_50: "2", + field_54: "6", + field_58: "7", + field_62: "8", + field_66: "9", + field_70: "0", + field_74: "10", + } + end + + it "sets value from correct mapping", aggregate_failures: true do + expect(parser.log.ecstat1).to eq(1) + expect(parser.log.ecstat2).to eq(2) + expect(parser.log.ecstat3).to eq(6) + expect(parser.log.ecstat4).to eq(7) + expect(parser.log.ecstat5).to eq(8) + expect(parser.log.ecstat6).to eq(9) + expect(parser.log.ecstat7).to eq(0) + expect(parser.log.ecstat8).to eq(10) + end + end + + describe "#relatN fields" do + let(:attributes) do + { + bulk_upload:, + field_47: "P", + field_51: "C", + field_55: "X", + field_59: "R", + field_63: "P", + field_67: "C", + field_71: "X", + } + end + + it "sets value from correct mapping", aggregate_failures: true do + expect(parser.log.relat2).to eq("P") + expect(parser.log.relat3).to eq("C") + expect(parser.log.relat4).to eq("X") + expect(parser.log.relat5).to eq("R") + expect(parser.log.relat6).to eq("P") + expect(parser.log.relat7).to eq("C") + expect(parser.log.relat8).to eq("X") + end + end + + describe "#net_income_known" do + context "when 1" do + let(:attributes) { { bulk_upload:, field_117: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.net_income_known).to eq(0) + end + end + + context "when 2" do + let(:attributes) { { bulk_upload:, field_117: "2" } } + + it "sets value from correct mapping" do + expect(parser.log.net_income_known).to eq(1) + end + end + + context "when 3" do + let(:attributes) { { bulk_upload:, field_117: "3" } } + + it "sets value from correct mapping" do + expect(parser.log.net_income_known).to eq(2) + end + end + end + + describe "#unitletas" do + let(:attributes) { { bulk_upload:, field_25: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.unitletas).to eq(1) + end + end + + describe "#rsnvac" do + let(:attributes) { { bulk_upload:, field_24: "5" } } + + it "sets value from correct mapping" do + expect(parser.log.rsnvac).to eq(5) + end + end + + describe "#sheltered" do + let(:attributes) { { bulk_upload:, field_41: "1" } } + + it "sets value from correct mapping" do + expect(parser.log.sheltered).to eq(1) + end + end + + describe "illness fields" do + mapping = [ + { attribute: :illness_type_1, field: :field_94 }, + { attribute: :illness_type_2, field: :field_88 }, + { attribute: :illness_type_3, field: :field_91 }, + { attribute: :illness_type_4, field: :field_86 }, + { attribute: :illness_type_5, field: :field_87 }, + { attribute: :illness_type_6, field: :field_89 }, + { attribute: :illness_type_7, field: :field_90 }, + { attribute: :illness_type_8, field: :field_93 }, + { attribute: :illness_type_9, field: :field_92 }, + { attribute: :illness_type_10, field: :field_95 }, + ] + + mapping.each do |hash| + describe "##{hash[:attribute]}" do + context "when yes" do + let(:attributes) { { bulk_upload:, hash[:field] => "1" } } + + it "sets value from correct mapping" do + expect(parser.log.public_send(hash[:attribute])).to eq(1) + end + end + + context "when no" do + let(:attributes) { { bulk_upload:, hash[:field] => "", field_4: 1 } } + + it "sets value from correct mapping" do + expect(parser.log.public_send(hash[:attribute])).to be_nil + end + end + end + end + end + + describe "#irproduct_other" do + let(:attributes) { { bulk_upload:, field_12: "some other product" } } + + it "sets value to given free text string" do + expect(parser.log.irproduct_other).to eql("some other product") + end + end + + describe "#tenancyother" do + let(:attributes) { { bulk_upload:, field_39: "some other tenancy" } } + + it "sets value to given free text string" do + expect(parser.log.tenancyother).to eql("some other tenancy") + end + end + + describe "#tenancylength" do + let(:attributes) { { bulk_upload:, field_40: "2" } } + + it "sets value to given free text string" do + expect(parser.log.tenancylength).to eq(2) + end + end + + describe "#earnings" do + let(:attributes) { { bulk_upload:, field_118: "104.50" } } + + it "rounds to the nearest whole pound" do + expect(parser.log.earnings).to eq(105) + end + end + + describe "#reasonother" do + let(:attributes) { { bulk_upload:, field_99: "some other reason" } } + + it "sets value to given free text string" do + expect(parser.log.reasonother).to eql("some other reason") + end + end + + describe "#ppcodenk" do + let(:attributes) { { bulk_upload:, field_102: "2" } } + + it "sets correct value from mapping" do + expect(parser.log.ppcodenk).to eq(1) + end + end + + describe "#household_charge" do + context "when log is general needs" do + let(:attributes) { { bulk_upload:, field_4: 1, field_122: "1" } } + + it "sets correct value from mapping" do + expect(parser.log.household_charge).to eq(nil) + end + end + + context "when log is supported housing" do + let(:attributes) { { bulk_upload:, field_4: 2, field_122: "1" } } + + it "sets correct value from mapping" do + expect(parser.log.household_charge).to eq(1) + end + end + end + + describe "#chcharge" do + let(:attributes) { { bulk_upload:, field_124: "123.45" } } + + it "sets value given" do + expect(parser.log.chcharge).to eq(123.45) + end + end + + describe "#supcharg" do + let(:attributes) { { bulk_upload:, field_128: "123.45" } } + + it "sets value given" do + expect(parser.log.supcharg).to eq(123.45) + end + end + + describe "#pscharge" do + let(:attributes) { { bulk_upload:, field_127: "123.45" } } + + it "sets value given" do + expect(parser.log.pscharge).to eq(123.45) + end + end + + describe "#scharge" do + let(:attributes) { { bulk_upload:, field_126: "123.45" } } + + it "sets value given" do + expect(parser.log.scharge).to eq(123.45) + end + end + + describe "#propcode" do + let(:attributes) { { bulk_upload:, field_14: "abc123" } } + + it "sets value given" do + expect(parser.log.propcode).to eq("abc123") + end + end + + describe "#mrcdate" do + context "when valid" do + let(:attributes) { { bulk_upload:, field_33: "13", field_34: "12", field_35: "22" } } + + it "sets value given" do + expect(parser.log.mrcdate).to eq(Date.new(2022, 12, 13)) + end + end + + context "when invalid" do + let(:attributes) { { bulk_upload:, field_33: "13", field_34: "13", field_35: "22" } } + + it "does not raise an error" do + expect { parser.log.mrcdate }.not_to raise_error + end + end + end + + describe "#majorrepairs" do + context "when mrcdate given" do + let(:attributes) { { bulk_upload:, field_33: "13", field_34: "12", field_35: "22" } } + + it "sets #majorrepairs to 1" do + expect(parser.log.majorrepairs).to eq(1) + end + end + + context "when mrcdate not given" do + let(:attributes) { { bulk_upload:, field_33: "", field_34: "", field_35: "", field_4: 1 } } + + it "sets #majorrepairs to 0" do + expect(parser.log.majorrepairs).to eq(0) + end + end + end + + describe "#voiddate" do + context "when valid" do + let(:attributes) { { bulk_upload:, field_30: "13", field_31: "12", field_32: "22" } } + + it "sets value given" do + expect(parser.log.voiddate).to eq(Date.new(2022, 12, 13)) + end + end + + context "when invalid" do + let(:attributes) { { bulk_upload:, field_30: "13", field_31: "13", field_32: "22" } } + + it "does not raise an error" do + expect { parser.log.voiddate }.not_to raise_error + end + end + end + + describe "#startdate" do + let(:attributes) { { bulk_upload:, field_8: now.day.to_s, field_9: now.month.to_s, field_10: now.strftime("%g") } } + + it "sets value given" do + expect(parser.log.startdate).to eq(now) + end + end + + describe "#postcode_full" do + let(:attributes) { { bulk_upload:, field_21: " EC1N ", field_22: " 2TD " } } + + it "strips whitespace" do + expect(parser.log.postcode_full).to eql("EC1N 2TD") + end + end + + describe "#la" do + let(:attributes) { { bulk_upload:, field_23: "E07000223" } } + + it "sets to given value" do + expect(parser.log.la).to eql("E07000223") + end + end + + describe "#prevloc" do + let(:attributes) { { bulk_upload:, field_105: "E07000223" } } + + it "sets to given value" do + expect(parser.log.prevloc).to eql("E07000223") + end + end + + describe "#previous_la_known" do + context "when known" do + let(:attributes) { { bulk_upload:, field_105: "E07000223" } } + + it "sets to 1" do + expect(parser.log.previous_la_known).to eq(1) + end + end + + context "when not known" do + let(:attributes) { { bulk_upload:, field_105: "", field_4: 1 } } + + it "sets to 0" do + expect(parser.log.previous_la_known).to eq(0) + end + end + end + + describe "#first_time_property_let_as_social_housing" do + context "when field_24 is 15, 16, or 17" do + let(:attributes) { { bulk_upload:, field_24: %w[15 16 17].sample } } + + it "sets to 1" do + expect(parser.log.first_time_property_let_as_social_housing).to eq(1) + end + end + + context "when field_24 is not 15, 16, or 17" do + let(:attributes) { { bulk_upload:, field_24: "1" } } + + it "sets to 0" do + expect(parser.log.first_time_property_let_as_social_housing).to eq(0) + end + end + end + + describe "#housingneeds" do + context "when no disabled needs" do + let(:attributes) { { bulk_upload:, field_83: "1" } } + + it "sets to 2" do + expect(parser.log.housingneeds).to eq(2) + end + end + + context "when dont know about disabled needs" do + let(:attributes) { { bulk_upload:, field_84: "1" } } + + it "sets to 3" do + expect(parser.log.housingneeds).to eq(3) + end + end + + context "when housingneeds are given" do + let(:attributes) { { bulk_upload:, field_83: "0", field_81: "1", field_82: "1" } } + + it "sets correct housingneeds" do + expect(parser.log.housingneeds).to eq(1) + expect(parser.log.housingneeds_type).to eq(2) + expect(parser.log.housingneeds_other).to eq(1) + end + end + + context "when housingneeds are given and field_82 is nil" do + let(:attributes) { { bulk_upload:, field_83: nil, field_81: "1", field_82: "1" } } + + it "sets correct housingneeds" do + expect(parser.log.housingneeds).to eq(1) + expect(parser.log.housingneeds_type).to eq(2) + expect(parser.log.housingneeds_other).to eq(1) + end + end + + context "when housingneeds are not given" do + let(:attributes) { { bulk_upload:, field_79: nil, field_80: nil, field_81: nil, field_83: nil } } + + it "sets correct housingneeds" do + expect(parser.log.housingneeds).to eq(1) + expect(parser.log.housingneeds_type).to eq(3) + end + end + + context "when housingneeds a and b are selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_80: "1" } } + + it "sets error on housingneeds a and b" do + parser.valid? + expect(parser.errors[:field_79]).to include("Only one disabled access need: fully wheelchair-accessible housing, wheelchair access to essential rooms or level access housing, can be selected") + expect(parser.errors[:field_80]).to include("Only one disabled access need: fully wheelchair-accessible housing, wheelchair access to essential rooms or level access housing, can be selected") + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when housingneeds a and c are selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_81: "1" } } + + it "sets error on housingneeds a and c" do + parser.valid? + expect(parser.errors[:field_79]).to include("Only one disabled access need: fully wheelchair-accessible housing, wheelchair access to essential rooms or level access housing, can be selected") + expect(parser.errors[:field_81]).to include("Only one disabled access need: fully wheelchair-accessible housing, wheelchair access to essential rooms or level access housing, can be selected") + expect(parser.errors[:field_80]).to be_blank + end + end + + context "when housingneeds b and c are selected" do + let(:attributes) { { bulk_upload:, field_80: "1", field_81: "1" } } + + it "sets error on housingneeds b and c" do + parser.valid? + expect(parser.errors[:field_80]).to include("Only one disabled access need: fully wheelchair-accessible housing, wheelchair access to essential rooms or level access housing, can be selected") + expect(parser.errors[:field_81]).to include("Only one disabled access need: fully wheelchair-accessible housing, wheelchair access to essential rooms or level access housing, can be selected") + expect(parser.errors[:field_79]).to be_blank + end + end + + context "when housingneeds a and g are selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_83: "1" } } + + it "sets error on housingneeds a and g" do + parser.valid? + expect(parser.errors[:field_83]).to include("No disabled access needs can’t be selected if you have selected fully wheelchair-accessible housing, wheelchair access to essential rooms, level access housing or other disabled access needs") + expect(parser.errors[:field_79]).to include("No disabled access needs can’t be selected if you have selected fully wheelchair-accessible housing, wheelchair access to essential rooms, level access housing or other disabled access needs") + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when only housingneeds g is selected" do + let(:attributes) { { bulk_upload:, field_79: "0", field_83: "1" } } + + it "does not add any housingneeds errors" do + parser.valid? + expect(parser.errors[:field_55]).to be_blank + expect(parser.errors[:field_79]).to be_blank + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when housingneeds a and h are selected" do + let(:attributes) { { bulk_upload:, field_79: "1", field_84: "1" } } + + it "sets error on housingneeds a and h" do + parser.valid? + expect(parser.errors[:field_84]).to include("Don’t know disabled access needs can’t be selected if you have selected fully wheelchair-accessible housing, wheelchair access to essential rooms, level access housing or other disabled access needs") + expect(parser.errors[:field_79]).to include("Don’t know disabled access needs can’t be selected if you have selected fully wheelchair-accessible housing, wheelchair access to essential rooms, level access housing or other disabled access needs") + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + + context "when only housingneeds h is selected" do + let(:attributes) { { bulk_upload:, field_79: "0", field_84: "1" } } + + it "does not add any housingneeds errors" do + parser.valid? + expect(parser.errors[:field_84]).to be_blank + expect(parser.errors[:field_79]).to be_blank + expect(parser.errors[:field_80]).to be_blank + expect(parser.errors[:field_81]).to be_blank + end + end + end + + describe "#housingneeds_type" do + context "when field_79 is 1" do + let(:attributes) { { bulk_upload:, field_79: "1" } } + + it "set to 0" do + expect(parser.log.housingneeds_type).to eq(0) + end + end + + context "when field_80 is 1" do + let(:attributes) { { bulk_upload:, field_80: "1" } } + + it "set to 1" do + expect(parser.log.housingneeds_type).to eq(1) + end + end + + context "when field_81 is 1" do + let(:attributes) { { bulk_upload:, field_81: "1" } } + + it "set to 2" do + expect(parser.log.housingneeds_type).to eq(2) + end + end + end + + describe "#housingneeds_other" do + context "when field_54 is 1" do + let(:attributes) { { bulk_upload:, field_82: "1" } } + + it "sets to 1" do + expect(parser.log.housingneeds_other).to eq(1) + end + end + end + end + + describe "#start_date" do + context "when year of 9 is passed to represent 2009" do + let(:attributes) { { bulk_upload:, field_8: "1", field_9: "1", field_10: "9" } } + + it "uses the year 2009" do + expect(parser.send(:start_date)).to eql(Date.new(2009, 1, 1)) + end + end + end + + describe "#spreadsheet_duplicate_hash" do + it "returns a hash" do + expect(parser.spreadsheet_duplicate_hash).to be_a(Hash) + end + end + + describe "#add_duplicate_found_in_spreadsheet_errors" do + it "adds errors" do + expect { parser.add_duplicate_found_in_spreadsheet_errors }.to change(parser.errors, :size) + end + end +end diff --git a/spec/support/bulk_upload/lettings_log_to_csv.rb b/spec/support/bulk_upload/lettings_log_to_csv.rb index 13dc7c738..4aaca922f 100644 --- a/spec/support/bulk_upload/lettings_log_to_csv.rb +++ b/spec/support/bulk_upload/lettings_log_to_csv.rb @@ -42,6 +42,15 @@ class BulkUpload::LettingsLogToCsv end end + def to_2024_csv_row(seed: nil) + if seed + row = to_2024_row.shuffle(random: Random.new(seed)) + (row_prefix + row).flatten.join(",") + line_ending + else + (row_prefix + to_2024_row).flatten.join(",") + line_ending + end + end + def to_2023_row to_2022_row + [ log.needstype, @@ -62,10 +71,169 @@ class BulkUpload::LettingsLogToCsv end.flatten.join(",") + line_ending end + def default_2024_field_numbers_row(seed: nil) + if seed + ["Bulk upload field number"] + default_2024_field_numbers.shuffle(random: Random.new(seed)) + else + ["Bulk upload field number"] + default_2024_field_numbers + end.flatten.join(",") + line_ending + end + def default_2023_field_numbers [5, nil, nil, 15, 16, nil, 13, 40, 41, 42, 43, 46, 52, 56, 60, 64, 68, 72, 76, 47, 53, 57, 61, 65, 69, 73, 77, 51, 55, 59, 63, 67, 71, 75, 50, 54, 58, 62, 66, 70, 74, 78, 48, 49, 79, 81, 82, 123, 124, 122, 120, 102, 103, nil, 83, 84, 85, 86, 87, 88, 104, 109, 107, 108, 106, 100, 101, 105, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 126, 128, 129, 130, 131, 132, 127, 125, 133, 134, 33, 34, 35, 36, 37, 38, nil, 7, 8, 9, 28, 14, 32, 29, 30, 31, 26, 27, 25, 23, 24, nil, 1, 3, 2, 80, nil, 121, 44, 89, 98, 92, 95, 90, 91, 93, 94, 97, 96, 99, 10, 11, 12, 45, 39, 6, 4, 17, 18, 19, 20, 21, 22] end + def default_2024_field_numbers + (1..130).to_a + end + + def to_2024_row + [ + log.owning_organisation&.old_visible_id, # 1 + log.managing_organisation&.old_visible_id, + log.created_by&.email, + log.needstype, + "S#{log.scheme&.id}", + log.location&.id, + renewal, + log.startdate&.day, + log.startdate&.month, + log.startdate&.strftime("%y"), # 10 + + london_affordable_rent, + log.irproduct_other, + log.tenancycode, + log.propcode, + log.declaration, + log.uprn, + log.address_line1, + log.address_line2, + log.town_or_city, + log.county, # 20 + + ((log.postcode_full || "").split(" ") || [""]).first, + ((log.postcode_full || "").split(" ") || [""]).last, + log.la, + log.rsnvac, + log.unitletas, + log.unittype_gn, + log.builtype, + log.wchair, + log.beds, + log.voiddate&.day, # 30 + + log.voiddate&.month, + log.voiddate&.strftime("%y"), + log.mrcdate&.day, + log.mrcdate&.month, + log.mrcdate&.strftime("%y"), + log.joint, + log.startertenancy, + log.tenancy, + log.tenancyother, + log.tenancylength, # 40 + + log.sheltered, + log.age1 || overrides[:age1], + log.sex1, + log.ethnic, + log.national, + log.ecstat1, + log.relat2, + log.age2 || overrides[:age2], + log.sex2, + log.ecstat2, # 50 + + log.relat3, + log.age3 || overrides[:age3], + log.sex3, + log.ecstat3, + log.relat4, + log.age4 || overrides[:age4], + log.sex4, + log.ecstat4, + log.relat5, + log.age5 || overrides[:age5], # 60 + + log.sex5, + log.ecstat5, + log.relat6, + log.age6 || overrides[:age6], + log.sex6, + log.ecstat6, + log.relat7, + log.age7 || overrides[:age7], + log.sex7, + log.ecstat7, # 70 + + log.relat8, + log.age8 || overrides[:age8], + log.sex8, + log.ecstat8, + log.armedforces, + log.leftreg, + log.reservist, + log.preg_occ, + log.housingneeds_a, + log.housingneeds_b, # 80 + + log.housingneeds_c, + log.housingneeds_f, + log.housingneeds_g, + log.housingneeds_h, + overrides[:illness] || log.illness, + log.illness_type_1, + log.illness_type_2, + log.illness_type_3, + log.illness_type_4, + log.illness_type_5, # 90 + + log.illness_type_6, + log.illness_type_7, + log.illness_type_8, + log.illness_type_9, + log.illness_type_10, + log.layear, + log.waityear, + log.reason, + log.reasonother, + log.prevten, # 100 + + homeless, + previous_postcode_known, + ((log.ppostcode_full || "").split(" ") || [""]).first, + ((log.ppostcode_full || "").split(" ") || [""]).last, + log.prevloc, + log.reasonpref, + log.rp_homeless, + log.rp_insan_unsat, + log.rp_medwel, + log.rp_hardship, # 110 + + log.rp_dontknow, + cbl, + chr, + cap, + nil, # accessible register + log.referral, + net_income_known, + log.earnings, + log.incfreq, + log.hb, # 120 + + log.benefits, + log.household_charge, + log.period, + log.chcharge, + log.brent, + log.scharge, + log.pscharge, + log.supcharg, + log.hbrentshortfall, + log.tshortfall, # 130 + ] + end + def to_2022_row [ log.renttype, # 1