diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 47ca80705..4554b99d2 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -148,7 +148,7 @@ class LettingsLog < Log OPTIONAL_FIELDS = %w[tenancycode propcode chcharge].freeze RENT_TYPE_MAPPING_LABELS = { 1 => "Social Rent", 2 => "Affordable Rent", 3 => "Intermediate Rent", 4 => "Specified accommodation" }.freeze HAS_BENEFITS_OPTIONS = [1, 6, 8, 7].freeze - NUM_OF_WEEKS_FROM_PERIOD = { 2 => 26, 3 => 13, 4 => 12, 5 => 50, 6 => 49, 7 => 48, 8 => 47, 9 => 46, 1 => 52, 10 => 53 }.freeze + NUM_OF_WEEKS_FROM_PERIOD = { 2 => 26, 3 => 13, 4 => 12, 5 => 50, 6 => 49, 7 => 48, 8 => 47, 9 => 46, 11 => 51, 1 => 52, 10 => 53 }.freeze SUFFIX_FROM_PERIOD = { 2 => "every 2 weeks", 3 => "every 4 weeks", 4 => "every month" }.freeze DUPLICATE_LOG_ATTRIBUTES = %w[owning_organisation_id tenancycode startdate age1_known age1 sex1 ecstat1 tcharge household_charge chcharge].freeze RENT_TYPE = { @@ -626,7 +626,7 @@ class LettingsLog < Log end def rent_and_charges_paid_weekly? - [1, 5, 6, 7, 8, 9, 10].include? period + [1, 5, 6, 7, 8, 9, 10, 11].include? period end def rent_and_charges_paid_every_4_weeks? diff --git a/app/models/validations/sales/financial_validations.rb b/app/models/validations/sales/financial_validations.rb index e5d8232f0..ae98184c9 100644 --- a/app/models/validations/sales/financial_validations.rb +++ b/app/models/validations/sales/financial_validations.rb @@ -75,7 +75,7 @@ module Validations::Sales::FinancialValidations if threshold && record.stairbought < threshold shared_ownership_type = record.form.get_question("type", record).label_from_value(record.type).downcase record.errors.add :stairbought, I18n.t("validations.sales.financial.stairbought.percentage_bought_must_be_at_least_threshold", threshold:, shared_ownership_type:) - record.errors.add :type, I18n.t("validations.sales.financial.type.percentage_bought_must_be_at_least_threshold", threshold:, shared_ownership_type:) + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.financial.type.percentage_bought_must_be_at_least_threshold", threshold:, shared_ownership_type:) end end @@ -96,10 +96,10 @@ module Validations::Sales::FinancialValidations return unless (range = ranges[record.type]) if record.equity < range.min - record.errors.add :type, I18n.t("validations.sales.financial.type.equity_under_min", min_equity: range.min) + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.financial.type.equity_under_min", min_equity: range.min) record.errors.add :equity, :under_min, message: I18n.t("validations.sales.financial.equity.equity_under_min", min_equity: range.min) elsif !record.is_resale? && record.equity > range.max - record.errors.add :type, I18n.t("validations.sales.financial.type.equity_over_max", max_equity: range.max) + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.financial.type.equity_over_max", max_equity: range.max) record.errors.add :equity, :over_max, message: I18n.t("validations.sales.financial.equity.equity_over_max", max_equity: range.max) record.errors.add :resale, I18n.t("validations.sales.financial.resale.equity_over_max", max_equity: range.max) end diff --git a/app/models/validations/sales/sale_information_validations.rb b/app/models/validations/sales/sale_information_validations.rb index f99f6668a..3ddecaedb 100644 --- a/app/models/validations/sales/sale_information_validations.rb +++ b/app/models/validations/sales/sale_information_validations.rb @@ -108,7 +108,7 @@ module Validations::Sales::SaleInformationValidations if record.shared_ownership_scheme? && !record.old_persons_shared_ownership? && record.mrent > 9999 record.errors.add :mrent, I18n.t("validations.sales.sale_information.mrent.monthly_rent_higher_than_expected") - record.errors.add :type, I18n.t("validations.sales.sale_information.type.monthly_rent_higher_than_expected") + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.sale_information.type.monthly_rent_higher_than_expected") end end @@ -136,7 +136,7 @@ module Validations::Sales::SaleInformationValidations if max_stairbought && record.stairbought > max_stairbought record.errors.add :stairbought, I18n.t("validations.sales.sale_information.stairbought.stairbought_over_max", max_stairbought:, type: record.form.get_question("type", record).answer_label(record)) - record.errors.add :type, I18n.t("validations.sales.sale_information.type.stairbought_over_max", max_stairbought:, type: record.form.get_question("type", record).answer_label(record)) + record.errors.add :type, :skip_bu_error, message: I18n.t("validations.sales.sale_information.type.stairbought_over_max", max_stairbought:, type: record.form.get_question("type", record).answer_label(record)) end end diff --git a/app/services/bulk_upload/sales/year2025/row_parser.rb b/app/services/bulk_upload/sales/year2025/row_parser.rb index dc0e31dfa..886b3e928 100644 --- a/app/services/bulk_upload/sales/year2025/row_parser.rb +++ b/app/services/bulk_upload/sales/year2025/row_parser.rb @@ -815,31 +815,11 @@ private attributes["sex5"] = field_52 attributes["sex6"] = field_56 - attributes["relat2"] = if field_34 == 1 - "P" - else - (field_34 == 2 ? "X" : "R") - end - attributes["relat3"] = if field_42 == 1 - "P" - else - (field_42 == 2 ? "X" : "R") - end - attributes["relat4"] = if field_46 == 1 - "P" - else - (field_46 == 2 ? "X" : "R") - end - attributes["relat5"] = if field_50 == 1 - "P" - else - (field_50 == 2 ? "X" : "R") - end - attributes["relat6"] = if field_54 == 1 - "P" - else - (field_54 == 2 ? "X" : "R") - end + attributes["relat2"] = relationship_from_is_partner(field_34) + attributes["relat3"] = relationship_from_is_partner(field_42) + attributes["relat4"] = relationship_from_is_partner(field_46) + attributes["relat5"] = relationship_from_is_partner(field_50) + attributes["relat6"] = relationship_from_is_partner(field_54) attributes["ecstat1"] = field_32 attributes["ecstat2"] = field_39 @@ -1052,6 +1032,17 @@ private field_55.present? || field_56.present? || field_54.present? end + def relationship_from_is_partner(is_partner) + case is_partner + when 1 + "P" + when 2 + "X" + when 3 + "R" + end + end + def details_known?(person_n) send("person_#{person_n}_present?") ? 1 : 2 end @@ -1491,6 +1482,17 @@ private %w[0] + GlobalConstants::COUNTRIES_ANSWER_OPTIONS.keys # 0 is "Prefers not to say" end + def validate_relat_fields + %i[field_34 field_42 field_46 field_50 field_54].each do |field| + value = send(field) + next if value.blank? + + unless (1..3).cover?(value) + errors.add(field, I18n.t("#{ERROR_BASE_KEY}.invalid_option", question: format_ending(QUESTIONS[field]))) + end + end + end + def bulk_upload_organisation Organisation.find(bulk_upload.organisation_id) end diff --git a/app/services/csv/lettings_log_csv_service.rb b/app/services/csv/lettings_log_csv_service.rb index d7ab101ac..2da392fb0 100644 --- a/app/services/csv/lettings_log_csv_service.rb +++ b/app/services/csv/lettings_log_csv_service.rb @@ -177,6 +177,10 @@ module Csv 10 => "Intermediate rent supported housing private registered provider", 11 => "Intermediate rent general needs local authority", 12 => "Intermediate rent supported housing local authority", + 13 => "Specified accommodation general needs private registered provider", + 14 => "Specified accommodation supported housing private registered provider", + 15 => "Specified accommodation general needs local authority", + 16 => "Specified accommodation supported housing local authority", }.freeze IRPRODUCT_LABELS = { @@ -206,6 +210,7 @@ module Csv 1 => "Social Rent", 2 => "Affordable Rent", 3 => "Intermediate Rent", + 4 => "Specified accommodation", }.freeze UPRN_KNOWN_LABELS = { diff --git a/app/services/exports/user_export_service.rb b/app/services/exports/user_export_service.rb index 707d13c14..177daee1d 100644 --- a/app/services/exports/user_export_service.rb +++ b/app/services/exports/user_export_service.rb @@ -28,7 +28,7 @@ module Exports def retrieve_resources(recent_export, full_update, _year) if !full_update && recent_export params = { from: recent_export.started_at, to: @start_time } - User.where("(updated_at >= :from AND updated_at <= :to)", params) + User.where("(updated_at >= :from AND updated_at <= :to) OR (values_updated_at IS NOT NULL AND values_updated_at >= :from AND values_updated_at <= :to)", params) else params = { to: @start_time } User.where("updated_at <= :to", params) diff --git a/app/services/merge/merge_organisations_service.rb b/app/services/merge/merge_organisations_service.rb index bcaad85a6..40749b54c 100644 --- a/app/services/merge/merge_organisations_service.rb +++ b/app/services/merge/merge_organisations_service.rb @@ -62,7 +62,7 @@ private def merge_users(merging_organisation) users_to_merge = users_to_merge(merging_organisation) @merged_users[merging_organisation.name] = users_to_merge.map { |user| { name: user.name, email: user.email } } - users_to_merge.update_all(organisation_id: @absorbing_organisation.id) + users_to_merge.update_all(organisation_id: @absorbing_organisation.id, values_updated_at: Time.zone.now) end def merge_schemes_and_locations(merging_organisation) diff --git a/db/migrate/20250305092900_add_values_updated_at_to_user.rb b/db/migrate/20250305092900_add_values_updated_at_to_user.rb new file mode 100644 index 000000000..8ebe67af4 --- /dev/null +++ b/db/migrate/20250305092900_add_values_updated_at_to_user.rb @@ -0,0 +1,5 @@ +class AddValuesUpdatedAtToUser < ActiveRecord::Migration[7.2] + def change + add_column :users, :values_updated_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 82d67b432..e29560461 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_02_25_180643) do +ActiveRecord::Schema[7.2].define(version: 2025_03_05_092900) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -853,6 +853,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_25_180643) do t.boolean "reactivate_with_organisation" t.datetime "discarded_at" t.string "phone_extension" + t.datetime "values_updated_at" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["encrypted_otp_secret_key"], name: "index_users_on_encrypted_otp_secret_key", unique: true diff --git a/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb b/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb index 28aac4fea..0074b3712 100644 --- a/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb +++ b/spec/services/bulk_upload/sales/year2025/row_parser_spec.rb @@ -60,7 +60,7 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do field_31: "28", field_32: "1", field_33: "1", - field_34: "R", + field_34: "3", field_35: "32", field_36: "F", field_37: "17", @@ -1145,6 +1145,52 @@ RSpec.describe BulkUpload::Sales::Year2025::RowParser do end end + describe "relationship field mappings" do + [ + %w[field_34 relat2 2], + %w[field_42 relat3 3], + %w[field_46 relat4 4], + %w[field_50 relat5 5], + %w[field_54 relat6 6], + ].each do |input_field, relationship_attribute, person_num| + describe input_field.to_s do + context "when #{input_field} is 1" do + let(:attributes) { setup_section_params.merge({ input_field.to_sym => "1", field_41: "5" }) } + + it "sets relationship to P" do + expect(parser.log.public_send(relationship_attribute)).to eq("P") + end + end + + context "when #{input_field} is 2" do + let(:attributes) { setup_section_params.merge({ input_field.to_sym => "2", field_41: "5" }) } + + it "sets relationship to X" do + expect(parser.log.public_send(relationship_attribute)).to eq("X") + end + end + + context "when #{input_field} is 3" do + let(:attributes) { setup_section_params.merge({ input_field.to_sym => "3", field_41: "5" }) } + + it "sets relationship to R" do + expect(parser.log.public_send(relationship_attribute)).to eq("R") + end + end + + context "when #{input_field} is 4" do + let(:attributes) { setup_section_params.merge({ input_field.to_sym => "4", field_41: "5" }) } + + it "gives a validation error" do + parser.valid? + validation_message = "You must answer person #{person_num} is the partner of buyer 1." + expect(parser.errors[input_field]).to include validation_message + end + end + end + end + end + describe "field_39" do # ecstat2 context "when buyer 2 has no age but has ecstat as child" do let(:attributes) { valid_attributes.merge({ field_35: nil, field_39: "9" }) } diff --git a/spec/services/exports/user_export_service_spec.rb b/spec/services/exports/user_export_service_spec.rb index 3f4ece76d..93ba124d9 100644 --- a/spec/services/exports/user_export_service_spec.rb +++ b/spec/services/exports/user_export_service_spec.rb @@ -202,7 +202,26 @@ RSpec.describe Exports::UserExportService do before do create(:user, updated_at: Time.zone.local(2022, 4, 27), organisation:) create(:user, updated_at: Time.zone.local(2022, 4, 27), organisation:) - Export.create!(started_at: Time.zone.local(2022, 4, 26), base_number: 1, increment_number: 1) + Export.create!(started_at: Time.zone.local(2022, 4, 26), base_number: 1, increment_number: 1, empty_export: true, collection: "users") + end + + it "generates an XML manifest file with the expected content within the ZIP file" do + expected_content = replace_record_number(local_manifest_file.read, 2) + expect(storage_service).to receive(:write_file).with(expected_zip_filename, any_args) do |_, content| + entry = Zip::File.open_buffer(content).find_entry(expected_manifest_filename) + expect(entry).not_to be_nil + expect(entry.get_input_stream.read).to eq(expected_content) + end + + expect(export_service.export_xml_users).to eq({ expected_zip_filename.gsub(".zip", "") => start_time }) + end + end + + context "and a user has been manually updated since the previous partial export" do + before do + create(:user, updated_at: Time.zone.local(2022, 4, 25), values_updated_at: Time.zone.local(2022, 4, 27), organisation:) + create(:user, updated_at: Time.zone.local(2022, 4, 25), values_updated_at: Time.zone.local(2022, 4, 27), organisation:) + Export.create!(started_at: Time.zone.local(2022, 4, 26), base_number: 1, increment_number: 1, empty_export: true, collection: "users") end it "generates an XML manifest file with the expected content within the ZIP file" do diff --git a/spec/services/merge/merge_organisations_service_spec.rb b/spec/services/merge/merge_organisations_service_spec.rb index f20e16a16..9355a757a 100644 --- a/spec/services/merge/merge_organisations_service_spec.rb +++ b/spec/services/merge/merge_organisations_service_spec.rb @@ -31,10 +31,12 @@ RSpec.describe Merge::MergeOrganisationsService do expect(Rails.logger).to receive(:info).with("\t#{merging_organisation.data_protection_officers.first.name} (#{merging_organisation.data_protection_officers.first.email})") expect(Rails.logger).to receive(:info).with("\tfake name (fake@email.com)") expect(Rails.logger).to receive(:info).with("New schemes from fake org:") + expect(merging_organisation_user.values_updated_at).to be_nil merge_organisations_service.call merging_organisation_user.reload expect(merging_organisation_user.organisation).to eq(absorbing_organisation) + expect(merging_organisation_user.values_updated_at).not_to be_nil end it "sets merge date on merged organisation" do