diff --git a/.gitignore b/.gitignore index 93a5a3a05..b2b589114 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ yarn-debug.log* .yarn-integrity .env +.env.development # Code coverage results /coverage diff --git a/app/controllers/locations_controller.rb b/app/controllers/locations_controller.rb index e55ee10d6..83e90c373 100644 --- a/app/controllers/locations_controller.rb +++ b/app/controllers/locations_controller.rb @@ -6,15 +6,18 @@ class LocationsController < ApplicationController before_action :find_location, except: %i[create index] before_action :find_scheme before_action :scheme_and_location_present, except: %i[create index] + before_action :session_filters, if: :current_user, only: %i[index] + before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index] before_action :authorize_user, except: %i[index create] def index authorize @scheme - @pagy, @locations = pagy(filtered_collection(@scheme.locations, search_term)) + @pagy, @locations = pagy(filter_manager.filtered_locations(@scheme.locations, search_term, session_filters)) @total_count = @scheme.locations.size @searched = search_term.presence + @filter_type = "scheme_locations" end def create @@ -170,7 +173,7 @@ class LocationsController < ApplicationController end def deactivate_confirm - @affected_logs = @location.lettings_logs.visible.filter_by_before_startdate(params[:deactivation_date]) + @affected_logs = @location.lettings_logs.visible.after_date(params[:deactivation_date]) if @affected_logs.count.zero? deactivate else @@ -271,7 +274,7 @@ private end def reset_location_and_scheme_for_logs! - logs = @location.lettings_logs.visible.filter_by_before_startdate(params[:deactivation_date].to_time) + logs = @location.lettings_logs.visible.after_date(params[:deactivation_date].to_time) logs.update!(location: nil, scheme: nil, unresolved: true) logs end @@ -297,4 +300,12 @@ private params[:referrer] == "check_answers" end helper_method :return_to_check_your_answers? + + def filter_manager + FilterManager.new(current_user:, session:, params:, filter_type: "scheme_locations") + end + + def session_filters + filter_manager.session_filters + end end diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index a433832b5..e026a70b9 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -51,7 +51,7 @@ class SchemesController < ApplicationController end def deactivate_confirm - @affected_logs = @scheme.lettings_logs.visible.filter_by_before_startdate(params[:deactivation_date]) + @affected_logs = @scheme.lettings_logs.visible.after_date(params[:deactivation_date]) if @affected_logs.count.zero? deactivate else @@ -335,7 +335,7 @@ private end def reset_location_and_scheme_for_logs! - logs = @scheme.lettings_logs.visible.filter_by_before_startdate(params[:deactivation_date].to_time) + logs = @scheme.lettings_logs.visible.after_date(params[:deactivation_date].to_time) logs.update!(location: nil, scheme: nil, unresolved: true) logs end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index cdcd05c9f..225f05f8b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,8 +1,9 @@ class SessionsController < ApplicationController def clear_filters session[session_name_for(params[:filter_type])] = "{}" + path_params = params[:path_params].presence || {} - redirect_to send("#{params[:filter_type]}_path") + redirect_to send("#{params[:filter_type]}_path", scheme_id: path_params[:scheme_id]) end private diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 5f619ad36..ac9a767f2 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -53,16 +53,21 @@ class UsersController < ApplicationController if @user == current_user bypass_sign_in @user flash[:notice] = I18n.t("devise.passwords.updated") if user_params.key?("password") + if user_params.key?("email") && FeatureToggle.new_email_journey? + flash[:notice] = I18n.t("devise.email.updated", email: @user.unconfirmed_email) + end + redirect_to account_path else user_name = @user.name&.possessive || @user.email.possessive - case user_params[:active] - when "false" + if user_params[:active] == "false" @user.update!(confirmed_at: nil, sign_in_count: 0, initial_confirmation_sent: false) flash[:notice] = I18n.t("devise.activation.deactivated", user_name:) - when "true" + elsif user_params[:active] == "true" @user.send_confirmation_instructions flash[:notice] = I18n.t("devise.activation.reactivated", user_name:) + elsif user_params.key?("email") && FeatureToggle.new_email_journey? + flash[:notice] = I18n.t("devise.email.updated", email: @user.unconfirmed_email) end redirect_to user_path(@user) end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index be2f7ced5..e2da5b0ed 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -56,6 +56,17 @@ module FiltersHelper }.freeze end + def location_status_filters + { + "incomplete" => "Incomplete", + "active" => "Active", + "deactivating_soon" => "Deactivating soon", + "activating_soon" => "Activating soon", + "reactivating_soon" => "Reactivating soon", + "deactivated" => "Deactivated", + }.freeze + end + def selected_option(filter, filter_type) return false unless session[session_name_for(filter_type)] @@ -80,9 +91,9 @@ module FiltersHelper applied_filters_count(filter_type).zero? ? "No filters applied" : "#{pluralize(applied_filters_count(filter_type), 'filter')} applied" end - def reset_filters_link(filter_type) + def reset_filters_link(filter_type, path_params = {}) if applied_filters_count(filter_type).positive? - govuk_link_to "Clear", clear_filters_path(filter_type:) + govuk_link_to "Clear", clear_filters_path(filter_type:, path_params:) end end @@ -91,6 +102,12 @@ module FiltersHelper [OpenStruct.new(id: "", name: "Select an option")] + organisation_options.map { |org| OpenStruct.new(id: org.id, name: org.name) } end + def show_scheme_managing_org_filter?(user) + org = user.organisation + + user.support? || org.stock_owners.count > 1 || (org.holds_own_stock? && org.stock_owners.count.positive?) + end + private def applied_filters_count(filter_type) @@ -98,6 +115,8 @@ private end def applied_filters(filter_type) + return {} unless session[session_name_for(filter_type)] + JSON.parse(session[session_name_for(filter_type)]) end diff --git a/app/mailers/devise_notify_mailer.rb b/app/mailers/devise_notify_mailer.rb index 5560e7925..533bd76b6 100644 --- a/app/mailers/devise_notify_mailer.rb +++ b/app/mailers/devise_notify_mailer.rb @@ -35,12 +35,17 @@ class DeviseNotifyMailer < Devise::Mailer end def confirmation_instructions(record, token, _opts = {}) - username = record.email - if email_changed(record) - username = record.unconfirmed_email - send_confirmation_email(record.unconfirmed_email, record, token, username) + if email_changed?(record) + if new_email_journey? + send_email_changed_to_old_email(record) + send_email_changed_to_new_email(record, token) + else + send_confirmation_email(record.unconfirmed_email, record, token, record.unconfirmed_email) + send_confirmation_email(record.email, record, token, record.unconfirmed_email) + end + else + send_confirmation_email(record.email, record, token, record.email) end - send_confirmation_email(record.email, record, token, username) end def intercept_send?(email) @@ -54,10 +59,48 @@ class DeviseNotifyMailer < Devise::Mailer Rails.application.credentials[:email_allowlist] || [] end -private + def send_email_changed_to_old_email(record) + return true if intercept_send?(record.email) - def email_changed(record) - record.confirmable_template == User::CONFIRMABLE_TEMPLATE_ID && (record.unconfirmed_email.present? && record.unconfirmed_email != record.email) + send_email( + record.email, + User::FOR_OLD_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID, + { + new_email: record.unconfirmed_email, + old_email: record.email, + }, + ) + end + + def send_email_changed_to_new_email(record, token) + return true if intercept_send?(record.email) + + link = "#{user_confirmation_url}?confirmation_token=#{token}" + + send_email( + record.unconfirmed_email, + User::FOR_NEW_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID, + { + new_email: record.unconfirmed_email, + old_email: record.email, + link:, + }, + ) + end + + def email_changed?(record) + ( + record.confirmable_template == User::CONFIRMABLE_TEMPLATE_ID && ( + record.unconfirmed_email.present? && record.unconfirmed_email != record.email) + ) || ( + new_email_journey? && + record.versions.last.changeset.key?("unconfirmed_email") && + record.confirmed? + ) + end + + def new_email_journey? + FeatureToggle.new_email_journey? end def send_confirmation_email(email, record, token, username) diff --git a/app/models/derived_variables/lettings_log_variables.rb b/app/models/derived_variables/lettings_log_variables.rb index d29f21ce2..960ecad52 100644 --- a/app/models/derived_variables/lettings_log_variables.rb +++ b/app/models/derived_variables/lettings_log_variables.rb @@ -128,6 +128,22 @@ private incref: 1, }, }, + { + conditions: { + net_income_known: 0, + }, + derived_values: { + incref: 0, + }, + }, + { + conditions: { + net_income_known: 1, + }, + derived_values: { + incref: 2, + }, + }, ].freeze def clear_inapplicable_derived_values! diff --git a/app/models/form/sales/questions/owning_organisation_id.rb b/app/models/form/sales/questions/owning_organisation_id.rb index 1643887d2..284e8b21f 100644 --- a/app/models/form/sales/questions/owning_organisation_id.rb +++ b/app/models/form/sales/questions/owning_organisation_id.rb @@ -37,6 +37,14 @@ class Form::Sales::Questions::OwningOrganisationId < ::Form::Question hsh end end + + user_answer_options = if user.support? + Organisation.where(holds_own_stock: true) + else + user.organisation.stock_owners + user.organisation.absorbed_organisations.where(holds_own_stock: true) + end.pluck(:id, :name).to_h + + answer_opts.merge(user_answer_options) end def displayed_answer_options(log, user = nil) @@ -73,6 +81,22 @@ class Form::Sales::Questions::OwningOrganisationId < ::Form::Question true end + def hidden_in_check_answers?(_log, user = nil) + return false if user.support? + + stock_owners = user.organisation.stock_owners + user.organisation.absorbed_organisations.where(holds_own_stock: true) + + if user.organisation.holds_own_stock? + stock_owners.count.zero? + else + stock_owners.count <= 1 + end + end + + def enabled + true + end + private def selected_answer_option_is_derived?(_log) diff --git a/app/models/lettings_log.rb b/app/models/lettings_log.rb index 0ac0e4bf5..2f11e31ff 100644 --- a/app/models/lettings_log.rb +++ b/app/models/lettings_log.rb @@ -50,7 +50,7 @@ class LettingsLog < Log .or(filter_by_postcode(param)) .or(filter_by_id(param)) } - scope :filter_by_before_startdate, ->(date) { where("lettings_logs.startdate >= ?", date) } + scope :after_date, ->(date) { where("lettings_logs.startdate >= ?", date) } scope :unresolved, -> { where(unresolved: true) } scope :filter_by_organisation, ->(org, _user = nil) { where(owning_organisation: org).or(where(managing_organisation: org)) } @@ -73,7 +73,7 @@ class LettingsLog < Log } AUTOGENERATED_FIELDS = %w[id status created_at updated_at discarded_at].freeze - OPTIONAL_FIELDS = %w[first_time_property_let_as_social_housing tenancycode propcode chcharge].freeze + OPTIONAL_FIELDS = %w[tenancycode propcode chcharge].freeze RENT_TYPE_MAPPING_LABELS = { 1 => "Social Rent", 2 => "Affordable Rent", 3 => "Intermediate Rent" }.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 diff --git a/app/models/location.rb b/app/models/location.rb index 0dd6ba522..f25fd7d5d 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -27,6 +27,56 @@ class Location < ApplicationRecord scope :active_in_2_weeks, -> { where(confirmed: true).and(started_in_2_weeks) } scope :confirmed, -> { where(confirmed: true) } scope :unconfirmed, -> { where.not(confirmed: true) } + scope :filter_by_status, lambda { |statuses, _user = nil| + filtered_records = all + scopes = [] + + statuses.each do |status| + if respond_to?(status, true) + scopes << (status == "active" ? send("active_status") : send(status)) + end + end + + if scopes.any? + filtered_records = filtered_records + .left_outer_joins(:location_deactivation_periods) + .order("location_deactivation_periods.created_at DESC") + .merge(scopes.reduce(&:or)) + end + + filtered_records + } + + scope :incomplete, lambda { + where.not(confirmed: true) + } + + scope :deactivated, lambda { + merge(LocationDeactivationPeriod.deactivations_without_reactivation) + .where("location_deactivation_periods.deactivation_date <= ?", Time.zone.now) + } + + scope :deactivating_soon, lambda { + merge(LocationDeactivationPeriod.deactivations_without_reactivation) + .where("location_deactivation_periods.deactivation_date > ?", Time.zone.now) + } + + scope :reactivating_soon, lambda { + where.not("location_deactivation_periods.reactivation_date IS NULL") + .where("location_deactivation_periods.reactivation_date > ?", Time.zone.now) + } + + scope :activating_soon, lambda { + where("startdate > ?", Time.zone.now) + } + + scope :active_status, lambda { + where.not(id: joins(:location_deactivation_periods).reactivating_soon.pluck(:id)) + .where.not(id: joins(:location_deactivation_periods).deactivated.pluck(:id)) + .where.not(id: incomplete.pluck(:id)) + .where.not(id: joins(:location_deactivation_periods).deactivating_soon.pluck(:id)) + .where.not(id: activating_soon.pluck(:id)) + } LOCAL_AUTHORITIES = LocalAuthority.all.map { |la| [la.name, la.code] }.to_h diff --git a/app/models/sales_log.rb b/app/models/sales_log.rb index fc57f3ba3..2a0a6d5c4 100644 --- a/app/models/sales_log.rb +++ b/app/models/sales_log.rb @@ -51,6 +51,7 @@ class SalesLog < Log .where.not(postcode_full: nil) .where("age1 IS NOT NULL OR age1_known = 1 OR age1_known = 2") } + scope :after_date, ->(date) { where("saledate >= ?", date) } OPTIONAL_FIELDS = %w[purchid othtype].freeze RETIREMENT_AGES = { "M" => 65, "F" => 60, "X" => 65 }.freeze diff --git a/app/models/scheme.rb b/app/models/scheme.rb index ff24abdad..4767b92cf 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -19,6 +19,7 @@ class Scheme < ApplicationRecord scope :order_by_completion, -> { order("schemes.confirmed ASC NULLS FIRST") } scope :order_by_service_name, -> { order(service_name: :asc) } + scope :filter_by_owning_organisation, ->(owning_organisation, _user = nil) { where(owning_organisation:) } scope :filter_by_status, lambda { |statuses, _user = nil| filtered_records = all scopes = [] diff --git a/app/models/user.rb b/app/models/user.rb index 48177d635..eccdaec47 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -121,6 +121,8 @@ class User < ApplicationRecord RECONFIRMABLE_TEMPLATE_ID = "bcdec787-f0a7-46e9-8d63-b3e0a06ee455".freeze BETA_ONBOARDING_TEMPLATE_ID = "b48bc2cd-5887-4611-8296-d0ab3ed0e7fd".freeze USER_REACTIVATED_TEMPLATE_ID = "ac45a899-490e-4f59-ae8d-1256fc0001f9".freeze + FOR_OLD_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID = "3eb80517-1051-4dfc-b4cc-cb18228a3829".freeze + FOR_NEW_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID = "0cdd0be1-7fa5-4808-8225-ae4c5a002352".freeze def reset_password_notify_template RESET_PASSWORD_TEMPLATE_ID @@ -131,7 +133,7 @@ class User < ApplicationRecord USER_REACTIVATED_TEMPLATE_ID elsif was_migrated_from_softwire? && last_sign_in_at.blank? BETA_ONBOARDING_TEMPLATE_ID - elsif initial_confirmation_sent + elsif initial_confirmation_sent && !confirmed? RECONFIRMABLE_TEMPLATE_ID else CONFIRMABLE_TEMPLATE_ID diff --git a/app/services/bulk_upload/lettings/year2022/row_parser.rb b/app/services/bulk_upload/lettings/year2022/row_parser.rb index 642c8efe8..9bef9312f 100644 --- a/app/services/bulk_upload/lettings/year2022/row_parser.rb +++ b/app/services/bulk_upload/lettings/year2022/row_parser.rb @@ -1466,6 +1466,7 @@ private def housingneeds_other return 1 if field_58 == 1 + return 0 if [field_55, field_56, field_57].include?(1) end def ethnic_group_from_ethnic diff --git a/app/services/bulk_upload/lettings/year2023/row_parser.rb b/app/services/bulk_upload/lettings/year2023/row_parser.rb index 4a12e9083..8d023792c 100644 --- a/app/services/bulk_upload/lettings/year2023/row_parser.rb +++ b/app/services/bulk_upload/lettings/year2023/row_parser.rb @@ -1410,6 +1410,7 @@ private def housingneeds_other return 1 if field_86 == 1 + return 0 if [field_83, field_84, field_85].include?(1) end def prevloc diff --git a/app/services/feature_toggle.rb b/app/services/feature_toggle.rb index d7df829ad..aed14d3e6 100644 --- a/app/services/feature_toggle.rb +++ b/app/services/feature_toggle.rb @@ -33,4 +33,8 @@ class FeatureToggle def self.deduplication_flow_enabled? !Rails.env.production? && !Rails.env.staging? end + + def self.new_email_journey? + !Rails.env.production? + end end diff --git a/app/services/filter_manager.rb b/app/services/filter_manager.rb index a473cf271..c8665752d 100644 --- a/app/services/filter_manager.rb +++ b/app/services/filter_manager.rb @@ -50,17 +50,29 @@ class FilterManager users end - def self.filter_schemes(schemes, search_term, filters, user) + def self.filter_schemes(schemes, search_term, filters, user, all_orgs) schemes = filter_by_search(schemes, search_term) filters.each do |category, values| next if Array(values).reject(&:empty?).blank? + next if category == "owning_organisation" && all_orgs schemes = schemes.public_send("filter_by_#{category}", values, user) end schemes end + def self.filter_locations(locations, search_term, filters, user) + locations = filter_by_search(locations, search_term) + + filters.each do |category, values| + next if Array(values).reject(&:empty?).blank? + + locations = locations.public_send("filter_by_#{category}", values, user) + end + locations.order(created_at: :desc) + end + def serialize_filters_to_session(specific_org: false) session[session_name_for(filter_type)] = session_filters(specific_org:).to_json end @@ -72,7 +84,8 @@ class FilterManager def deserialize_filters_from_session(specific_org) current_filters = session[session_name_for(filter_type)] new_filters = current_filters.present? ? JSON.parse(current_filters) : {} - if @filter_type.include?("logs") + + if filter_type.include?("logs") current_user.logs_filters(specific_org:).each do |filter| new_filters[filter] = params[filter] if params[filter].present? end @@ -84,10 +97,18 @@ class FilterManager new_filters["user"] = current_user.id.to_s if params["assigned_to"] == "you" end - if (@filter_type.include?("schemes") || @filter_type.include?("users")) && params["status"].present? + if (filter_type.include?("schemes") || filter_type.include?("users") || filter_type.include?("scheme_locations")) && params["status"].present? new_filters["status"] = params["status"] end + if filter_type.include?("schemes") + current_user.logs_filters(specific_org:).each do |filter| + new_filters[filter] = params[filter] if params[filter].present? + end + + new_filters = new_filters.except("owning_organisation") if params["owning_organisation_select"] == "all" + end + new_filters end @@ -102,7 +123,13 @@ class FilterManager end def filtered_schemes(schemes, search_term, filters) - FilterManager.filter_schemes(schemes, search_term, filters, current_user) + all_orgs = params["owning_organisation_select"] == "all" + + FilterManager.filter_schemes(schemes, search_term, filters, current_user, all_orgs) + end + + def filtered_locations(locations, search_term, filters) + FilterManager.filter_locations(locations, search_term, filters, current_user) end def bulk_upload diff --git a/app/services/imports/import_report_service.rb b/app/services/imports/import_report_service.rb new file mode 100644 index 000000000..61d43a9eb --- /dev/null +++ b/app/services/imports/import_report_service.rb @@ -0,0 +1,57 @@ +module Imports + class ImportReportService + def initialize(storage_service, institutions_csv, logger = Rails.logger) + @storage_service = storage_service + @logger = logger + @institutions_csv = institutions_csv + end + + BYTE_ORDER_MARK = "\uFEFF".freeze # Required to ensure Excel always reads CSV as UTF-8 + + def create_reports(report_suffix) + generate_missing_data_coordinators_report(report_suffix) + generate_logs_report(report_suffix) + end + + def generate_missing_data_coordinators_report(report_suffix) + report_csv = "Organisation ID,Old Organisation ID,Organisation Name\n" + organisations = @institutions_csv.map { |row| Organisation.find_by(name: row[0]) }.compact + organisations.each do |organisation| + if organisation.users.none? { |user| user.data_coordinator? && user.active? } + report_csv += "#{organisation.id},#{organisation.old_visible_id},#{organisation.name}\n" + end + end + + report_name = "OrganisationsWithoutDataCoordinators_#{report_suffix}" + @storage_service.write_file(report_name, BYTE_ORDER_MARK + report_csv) + + @logger.info("Missing data coordinators report available in s3 import bucket at #{report_name}") + end + + def generate_logs_report(report_suffix) + Rails.logger.info("Generating migrated logs report") + + rep = CSV.generate do |report| + headers = ["Institution name", "Id", "Old Completed lettings logs", "Old In progress lettings logs", "Old Completed sales logs", "Old In progress sales logs", "New Completed lettings logs", "New In Progress lettings logs", "New Completed sales logs", "New In Progress sales logs"] + report << headers + + @institutions_csv.each do |row| + name = row[0] + organisation = Organisation.find_by(name:) + next unless organisation + + completed_sales_logs = organisation.owned_sales_logs.where(status: "completed").count + in_progress_sales_logs = organisation.owned_sales_logs.where(status: "in_progress").count + completed_lettings_logs = organisation.owned_lettings_logs.where(status: "completed").count + in_progress_lettings_logs = organisation.owned_lettings_logs.where(status: "in_progress").count + report << row.push(completed_lettings_logs, in_progress_lettings_logs, completed_sales_logs, in_progress_sales_logs) + end + end + + report_name = "MigratedLogsReport_#{report_suffix}" + @storage_service.write_file(report_name, BYTE_ORDER_MARK + rep) + + @logger.info("Logs report available in s3 import bucket at #{report_name}") + end + end +end diff --git a/app/services/merge/merge_organisations_service.rb b/app/services/merge/merge_organisations_service.rb new file mode 100644 index 000000000..5a417ef02 --- /dev/null +++ b/app/services/merge/merge_organisations_service.rb @@ -0,0 +1,131 @@ +class Merge::MergeOrganisationsService + def initialize(absorbing_organisation_id:, merging_organisation_ids:) + @absorbing_organisation = Organisation.find(absorbing_organisation_id) + @merging_organisations = Organisation.find(merging_organisation_ids) + end + + def call + ActiveRecord::Base.transaction do + @merged_users = {} + @merged_schemes = {} + merge_organisation_details + @merging_organisations.each do |merging_organisation| + merge_rent_periods(merging_organisation) + merge_organisation_relationships(merging_organisation) + merge_users(merging_organisation) + merge_schemes_and_locations(merging_organisation) + merge_lettings_logs(merging_organisation) + merge_sales_logs(merging_organisation) + mark_organisation_as_merged(merging_organisation) + end + @absorbing_organisation.save! + log_success_message + rescue ActiveRecord::RecordInvalid => e + Rails.logger.error("Organisation merge failed with: #{e.message}") + raise ActiveRecord::Rollback + end + end + +private + + def merge_organisation_details + @absorbing_organisation.holds_own_stock = merge_boolean_organisation_attribute("holds_own_stock") + end + + def merge_rent_periods(merging_organisation) + merging_organisation.rent_periods.each do |rent_period| + @absorbing_organisation.organisation_rent_periods << OrganisationRentPeriod.new(rent_period:) unless @absorbing_organisation.rent_periods.include?(rent_period) + end + end + + def merge_organisation_relationships(merging_organisation) + merging_organisation.parent_organisation_relationships.each do |parent_organisation_relationship| + if parent_relationship_exists_on_absorbing_organisation?(parent_organisation_relationship) + parent_organisation_relationship.destroy! + else + parent_organisation_relationship.update!(child_organisation: @absorbing_organisation) + end + end + merging_organisation.child_organisation_relationships.each do |child_organisation_relationship| + if child_relationship_exists_on_absorbing_organisation?(child_organisation_relationship) + child_organisation_relationship.destroy! + else + child_organisation_relationship.update!(parent_organisation: @absorbing_organisation) + end + end + end + + def merge_users(merging_organisation) + @merged_users[merging_organisation.name] = merging_organisation.users.map { |user| { name: user.name, email: user.email } } + merging_organisation.users.update_all(organisation_id: @absorbing_organisation.id) + end + + def merge_schemes_and_locations(merging_organisation) + @merged_schemes[merging_organisation.name] = [] + merging_organisation.owned_schemes.each do |scheme| + next if scheme.deactivated? + + new_scheme = Scheme.create!(scheme.attributes.except("id", "owning_organisation_id").merge(owning_organisation: @absorbing_organisation)) + scheme.locations.each do |location| + new_scheme.locations << Location.new(location.attributes.except("id", "scheme_id")) unless location.deactivated? + end + @merged_schemes[merging_organisation.name] << { name: new_scheme.service_name, code: new_scheme.id } + SchemeDeactivationPeriod.create!(scheme:, deactivation_date: Time.zone.now) + end + end + + def merge_lettings_logs(merging_organisation) + merging_organisation.owned_lettings_logs.after_date(Time.zone.today).each do |lettings_log| + if lettings_log.scheme.present? + scheme_to_set = @absorbing_organisation.owned_schemes.find_by(service_name: lettings_log.scheme.service_name) + location_to_set = scheme_to_set.locations.find_by(name: lettings_log.location&.name, postcode: lettings_log.location&.postcode) + + lettings_log.scheme = scheme_to_set if scheme_to_set.present? + lettings_log.location = location_to_set if location_to_set.present? + end + lettings_log.owning_organisation = @absorbing_organisation + lettings_log.save! + end + merging_organisation.managed_lettings_logs.after_date(Time.zone.today).each do |lettings_log| + lettings_log.managing_organisation = @absorbing_organisation + lettings_log.save! + end + end + + def merge_sales_logs(merging_organisation) + merging_organisation.sales_logs.after_date(Time.zone.today).each do |sales_log| + sales_log.update(owning_organisation: @absorbing_organisation) + end + end + + def mark_organisation_as_merged(merging_organisation) + merging_organisation.update(merge_date: Time.zone.today, absorbing_organisation: @absorbing_organisation) + end + + def log_success_message + @merged_users.each do |organisation_name, users| + Rails.logger.info("Merged users from #{organisation_name}:") + users.each do |user| + Rails.logger.info("\t#{user[:name]} (#{user[:email]})") + end + end + @merged_schemes.each do |organisation_name, schemes| + Rails.logger.info("New schemes from #{organisation_name}:") + schemes.each do |scheme| + Rails.logger.info("\t#{scheme[:name]} (S#{scheme[:code]})") + end + end + end + + def merge_boolean_organisation_attribute(attribute) + @absorbing_organisation[attribute] ||= @merging_organisations.any? { |merging_organisation| merging_organisation[attribute] } + end + + def parent_relationship_exists_on_absorbing_organisation?(parent_organisation_relationship) + parent_organisation_relationship.parent_organisation == @absorbing_organisation || @absorbing_organisation.parent_organisation_relationships.where(parent_organisation: parent_organisation_relationship.parent_organisation).exists? + end + + def child_relationship_exists_on_absorbing_organisation?(child_organisation_relationship) + child_organisation_relationship.child_organisation == @absorbing_organisation || @absorbing_organisation.child_organisation_relationships.where(child_organisation: child_organisation_relationship.child_organisation).exists? + end +end diff --git a/app/views/bulk_upload_lettings_results/resume.html.erb b/app/views/bulk_upload_lettings_results/resume.html.erb index a5af1bd9d..15a670b0f 100644 --- a/app/views/bulk_upload_lettings_results/resume.html.erb +++ b/app/views/bulk_upload_lettings_results/resume.html.erb @@ -8,4 +8,4 @@ You’ve completed all the logs that had errors from your bulk upload.

-<%= govuk_button_link_to "Back to all logs", lettings_logs_path, button: true %> +<%= govuk_button_link_to "Return to lettings logs", clear_filters_path(filter_type: "lettings_logs"), button: true %> diff --git a/app/views/bulk_upload_sales_results/resume.html.erb b/app/views/bulk_upload_sales_results/resume.html.erb index f0e243f27..cd5b4a755 100644 --- a/app/views/bulk_upload_sales_results/resume.html.erb +++ b/app/views/bulk_upload_sales_results/resume.html.erb @@ -8,4 +8,4 @@ You’ve completed all the logs that had errors from your bulk upload.

-<%= govuk_button_link_to "Back to all logs", sales_logs_path, button: true %> +<%= govuk_button_link_to "Return to sales logs", clear_filters_path(filter_type: "sales_logs"), button: true %> diff --git a/app/views/locations/_location_filters.html.erb b/app/views/locations/_location_filters.html.erb new file mode 100644 index 000000000..d5bdeb156 --- /dev/null +++ b/app/views/locations/_location_filters.html.erb @@ -0,0 +1,30 @@ +
+
+
+

Filters

+
+ +
+ <%= form_with url: scheme_locations_path(@scheme), html: { method: :get } do |f| %> +
+

+ <%= filters_applied_text(@filter_type) %> +

+

+ <%= reset_filters_link(@filter_type, { scheme_id: @scheme.id }) %> +

+
+ + <%= render partial: "filters/checkbox_filter", + locals: { + f:, + options: location_status_filters, + label: "Status", + category: "status", + } %> + + <%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %> + <% end %> +
+
+
diff --git a/app/views/locations/index.html.erb b/app/views/locations/index.html.erb index 7641bbd48..ac6e3a0df 100644 --- a/app/views/locations/index.html.erb +++ b/app/views/locations/index.html.erb @@ -9,21 +9,19 @@ <% end %> <%= render partial: "organisations/headings", locals: { main: @scheme.service_name, sub: nil } %> +
-
-
- <%= render SubNavigationComponent.new(items: scheme_items(request.path, @scheme.id, "Locations")) %> + <%= render SubNavigationComponent.new(items: scheme_items(request.path, @scheme.id, "Locations")) %> + <%= render partial: "locations/location_filters" %> +

Locations

+
<%= render SearchComponent.new(current_user:, search_label: "Search by location name or postcode", value: @searched) %> <%= govuk_section_break(visible: true, size: "m") %> -
-
-
-
<%= govuk_table do |table| %> <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> <%= render(SearchResultCaptionComponent.new(searched: @searched, count: @pagy.count, item_label:, total_count: @total_count, item: "locations", path: request.path)) %> diff --git a/app/views/logs/_log_filters.html.erb b/app/views/logs/_log_filters.html.erb index cc3e3b4fb..1034379db 100644 --- a/app/views/logs/_log_filters.html.erb +++ b/app/views/logs/_log_filters.html.erb @@ -55,7 +55,7 @@ type: "select", label: "User", category: "user", - options: assigned_to_filter_options(@current_user), + options: assigned_to_filter_options(current_user), }, }, }, @@ -63,7 +63,7 @@ category: "assigned_to", } %> - <% if @current_user.support? || @current_user.organisation.stock_owners.count > 1 && request.path == "/lettings-logs" %> + <% if current_user.support? || current_user.organisation.stock_owners.count > 1 && request.path == "/lettings-logs" %> <%= render partial: "filters/radio_filter", locals: { f:, options: { @@ -74,7 +74,7 @@ type: "select", label: "Owning Organisation", category: "owning_organisation", - options: owning_organisation_filter_options(@current_user), + options: owning_organisation_filter_options(current_user), }, }, }, @@ -83,7 +83,7 @@ } %> <% end %> - <% if (@current_user.support? || @current_user.organisation.managing_agents.count > 1) && request.path == "/lettings-logs" %> + <% if (current_user.support? || current_user.organisation.managing_agents.count > 1) && request.path == "/lettings-logs" %> <%= render partial: "filters/radio_filter", locals: { f:, options: { @@ -94,7 +94,7 @@ type: "select", label: "Managed by", category: "managing_organisation", - options: managing_organisation_filter_options(@current_user), + options: managing_organisation_filter_options(current_user), }, }, }, diff --git a/app/views/organisations/show.html.erb b/app/views/organisations/show.html.erb index cc504c9f5..47b023fbd 100644 --- a/app/views/organisations/show.html.erb +++ b/app/views/organisations/show.html.erb @@ -37,7 +37,7 @@ <%= data_sharing_agreement_row(organisation: @organisation, user: current_user, summary_list:) %> <% end %> <% if FeatureToggle.merge_organisations_enabled? %> -

Is your organisation merging with another? <%= govuk_link_to "Let us know using this form", merge_request_organisation_path(@organisation) %>

+

To report a merge or update your organisation details, <%= govuk_link_to "contact the helpdesk", "https://dluhcdigital.atlassian.net/servicedesk/customer/portal/6/group/11" %>.

<% end %>
diff --git a/app/views/schemes/_scheme_filters.html.erb b/app/views/schemes/_scheme_filters.html.erb index 086d6fd84..e723f7469 100644 --- a/app/views/schemes/_scheme_filters.html.erb +++ b/app/views/schemes/_scheme_filters.html.erb @@ -22,6 +22,27 @@ label: "Status", category: "status", } %> + + <% if show_scheme_managing_org_filter?(current_user) %> + <%= render partial: "filters/radio_filter", locals: { + f:, + options: { + "all": { label: "Any owning organisation" }, + "specific_org": { + label: "Specific owning organisation", + conditional_filter: { + type: "select", + label: "Owning Organisation", + category: "owning_organisation", + options: owning_organisation_filter_options(current_user), + }, + }, + }, + label: "Owned by", + category: "owning_organisation_select", + } %> + <% end %> + <%= f.govuk_submit "Apply filters", class: "govuk-!-margin-bottom-0" %> <% end %>
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 844a0c965..bcdfd02b2 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -111,12 +111,11 @@ <%= govuk_button_to "Resend invite link", resend_invite_user_path(@user), secondary: true %> <% end %> <% else %> - - This user has been deactivated. <%= govuk_button_link_to "Reactivate user", reactivate_user_path(@user) %> - + + This user has been deactivated. <%= govuk_button_link_to "Reactivate user", reactivate_user_path(@user) %> + <% end %> <% end %>
-
diff --git a/config/locales/en.yml b/config/locales/en.yml index fb29431e1..5b7654c12 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -684,6 +684,8 @@ Make sure these answers are correct." hint_text: "This is more than 5 times the income, which is higher than we would expect." devise: + email: + updated: An email has been sent to %{email} to confirm this change. two_factor_authentication: success: "Two-factor authentication successful" attempt_failed: "Attempt failed" diff --git a/db/schema.rb b/db/schema.rb index ee5c1c1ad..7b5167a24 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.0].define(version: 2023_07_25_081029) do +ActiveRecord::Schema[7.0].define(version: 2023_07_19_150610) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/lib/tasks/correct_incref_values.rake b/lib/tasks/correct_incref_values.rake new file mode 100644 index 000000000..0b1921998 --- /dev/null +++ b/lib/tasks/correct_incref_values.rake @@ -0,0 +1,6 @@ +desc "Alter incref values for non imported lettings logs in the database" +task correct_incref_values: :environment do + LettingsLog.where(old_id: nil, net_income_known: 0).update!(incref: 0) + LettingsLog.where(old_id: nil, net_income_known: 1).update!(incref: 2) + LettingsLog.where(old_id: nil, net_income_known: 2).update!(incref: 1) +end diff --git a/lib/tasks/full_import.rake b/lib/tasks/full_import.rake index 780fe46fc..28af8c4dc 100644 --- a/lib/tasks/full_import.rake +++ b/lib/tasks/full_import.rake @@ -94,41 +94,19 @@ namespace :import do end desc "Generate migrated logs report" - task :generate_report, %i[institutions_csv_name] => :environment do |_task, args| + task :generate_reports, %i[institutions_csv_name] => :environment do |_task, args| institutions_csv_name = args[:institutions_csv_name] - raise "Usage: rake import:generate_report['institutions_csv_name']" if institutions_csv_name.blank? + raise "Usage: rake import:generate_reports['institutions_csv_name']" if institutions_csv_name.blank? s3_service = Storage::S3Service.new(Configuration::PaasConfigurationService.new, ENV["IMPORT_PAAS_INSTANCE"]) - csv = CSV.parse(s3_service.get_file_io(institutions_csv_name), headers: true) - - Rails.logger.info("Generating migrated logs report") - - rep = CSV.generate do |report| - headers = ["Institution name", "Id", "Old Completed lettings logs", "Old In progress lettings logs", "Old Completed sales logs", "Old In progress sales logs", "New Completed lettings logs", "New In Progress lettings logs", "New Completed sales logs", "New In Progress sales logs"] - report << headers - - csv.each do |row| - name = row[0] - organisation = Organisation.find_by(name:) - next unless organisation - - completed_sales_logs = organisation.owned_sales_logs.where(status: "completed").count - in_progress_sales_logs = organisation.owned_sales_logs.where(status: "in_progress").count - completed_lettings_logs = organisation.owned_lettings_logs.where(status: "completed").count - in_progress_lettings_logs = organisation.owned_lettings_logs.where(status: "in_progress").count - report << row.push(completed_lettings_logs, in_progress_lettings_logs, completed_sales_logs, in_progress_sales_logs) - end - end - - report_name = "MigratedLogsReport_#{institutions_csv_name}" - s3_service.write_file(report_name, rep) + institutions_csv = CSV.parse(s3_service.get_file_io(institutions_csv_name), headers: true) - Rails.logger.info("Logs report available in s3 import bucket at #{report_name}") + Imports::ImportReportService.new(s3_service, institutions_csv).create_reports(institutions_csv_name) end desc "Run import from logs step to end" - task :logs_onwards, %i[institutions_csv_name] => %i[environment logs trigger_invites generate_report] + task :logs_onwards, %i[institutions_csv_name] => %i[environment logs trigger_invites generate_reports] desc "Run a full import for the institutions listed in the named file on s3" - task :full, %i[institutions_csv_name] => %i[environment initial logs trigger_invites generate_report] + task :full, %i[institutions_csv_name] => %i[environment initial logs trigger_invites generate_reports] end diff --git a/lib/tasks/merge_organisations.rake b/lib/tasks/merge_organisations.rake new file mode 100644 index 000000000..820343883 --- /dev/null +++ b/lib/tasks/merge_organisations.rake @@ -0,0 +1,12 @@ +namespace :merge do + desc "Merge organisations into one" + task :merge_organisations, %i[absorbing_organisation_id merging_organisation_ids] => :environment do |_task, args| + absorbing_organisation_id = args[:absorbing_organisation_id] + merging_organisation_ids = args[:merging_organisation_ids]&.split(" ")&.map(&:to_i) + + raise "Usage: rake merge:merge_organisations[absorbing_organisation_id, merging_organisation_ids]" if merging_organisation_ids.blank? || absorbing_organisation_id.blank? + + service = Merge::MergeOrganisationsService.new(absorbing_organisation_id:, merging_organisation_ids:) + service.call + end +end diff --git a/spec/factories/location.rb b/spec/factories/location.rb index f43da0ac8..00fa0d77f 100644 --- a/spec/factories/location.rb +++ b/spec/factories/location.rb @@ -21,6 +21,11 @@ FactoryBot.define do old_visible_id { "111" } end + trait :incomplete do + units { nil } + confirmed { false } + end + trait :with_old_visible_id do old_visible_id { rand(9_999_999).to_s } end diff --git a/spec/features/schemes_spec.rb b/spec/features/schemes_spec.rb index c88b831ec..73972c4c3 100644 --- a/spec/features/schemes_spec.rb +++ b/spec/features/schemes_spec.rb @@ -284,6 +284,43 @@ RSpec.describe "Schemes scheme Features" do end end + context "when filtering locations" do + before do + click_link("Locations") + end + + context "when no filters are selected" do + it "displays the filters component with no clear button" do + expect(page).to have_content("No filters applied") + expect(page).not_to have_content("Clear") + end + end + + context "when I have selected filters" do + before do + check("Active") + check("Incomplete") + click_button("Apply filters") + end + + it "displays the filters component with a correct count and clear button" do + expect(page).to have_content("2 filters applied") + expect(page).to have_content("Clear") + end + + context "when clearing the filters" do + before do + click_link("Clear") + end + + it "clears the filters and displays the filter component as before" do + expect(page).to have_content("No filters applied") + expect(page).not_to have_content("Clear") + end + end + end + end + context "when the user clicks add location" do before do click_link("Locations") diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb index f568ef96b..e1cb65f66 100644 --- a/spec/features/user_spec.rb +++ b/spec/features/user_spec.rb @@ -529,7 +529,7 @@ RSpec.describe "User Features" do end before do - other_user.update!(initial_confirmation_sent: true) + other_user.update!(initial_confirmation_sent: true, confirmed_at: nil) allow(user).to receive(:need_two_factor_authentication?).and_return(false) sign_in(user) visit(user_path(user.id)) diff --git a/spec/helpers/filters_helper_spec.rb b/spec/helpers/filters_helper_spec.rb index 6ab012907..da36068b1 100644 --- a/spec/helpers/filters_helper_spec.rb +++ b/spec/helpers/filters_helper_spec.rb @@ -290,4 +290,74 @@ RSpec.describe FiltersHelper do end end end + + describe "#show_scheme_managing_org_filter?" do + context "when support user" do + let(:user) { create(:user, :support, organisation: create(:organisation, stock_owners: [])) } + + it "returns true" do + expect(show_scheme_managing_org_filter?(user)).to be true + end + end + + context "when not support user" do + let(:stock_owner1) { create(:organisation) } + let(:stock_owner2) { create(:organisation) } + + context "when org's stock_owners > 1" do + let(:user) { create(:user, organisation: create(:organisation, holds_own_stock: false)) } + + before do + create( + :organisation_relationship, + child_organisation: user.organisation, + parent_organisation: stock_owner1, + ) + create( + :organisation_relationship, + child_organisation: user.organisation, + parent_organisation: stock_owner2, + ) + end + + it "returns true" do + expect(show_scheme_managing_org_filter?(user)).to be true + end + end + + context "when org's stock_owners == 1" do + before do + create( + :organisation_relationship, + child_organisation: user.organisation, + parent_organisation: stock_owner1, + ) + end + + context "when holds own stock" do + let(:user) { create(:user, organisation: create(:organisation, holds_own_stock: true)) } + + it "returns true" do + expect(show_scheme_managing_org_filter?(user)).to be true + end + end + + context "when does not hold own stock" do + let(:user) { create(:user, organisation: create(:organisation, holds_own_stock: false)) } + + it "returns false" do + expect(show_scheme_managing_org_filter?(user)).to be false + end + end + end + + context "when org's stock_owners == 0" do + let(:user) { create(:user) } + + it "returns false" do + expect(show_scheme_managing_org_filter?(user)).to be false + end + end + end + end end diff --git a/spec/lib/tasks/correct_incref_values_spec.rb b/spec/lib/tasks/correct_incref_values_spec.rb new file mode 100644 index 000000000..fc046dcf6 --- /dev/null +++ b/spec/lib/tasks/correct_incref_values_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" +require "rake" + +RSpec.describe "correct_incref_values" do + describe ":correct_incref_values", type: :task do + subject(:task) { Rake::Task["correct_incref_values"] } + + before do + Rake.application.rake_require("tasks/correct_incref_values") + Rake::Task.define_task(:environment) + task.reenable + end + + context "when the rake task is run" do + let!(:lettings_log) { create(:lettings_log, :completed) } + + it "updates lettings logs with net_income_known 0 (yes) to have incref 0 (no)" do + lettings_log.update!(net_income_known: 0, incref: nil) + task.invoke + expect(lettings_log.reload.incref).to eq(0) + end + + it "updates lettings logs with net_income_known 1 (no) to have incref 2 (don't know)" do + lettings_log.update!(net_income_known: 1, incref: nil) + task.invoke + expect(lettings_log.reload.incref).to eq(2) + end + + it "updates lettings logs with net_income_known 2 (prefers not to say) to have incref 1 (yes)" do + lettings_log.update!(net_income_known: 2, incref: nil) + task.invoke + expect(lettings_log.reload.incref).to eq(1) + end + end + end +end diff --git a/spec/lib/tasks/full_import_spec.rb b/spec/lib/tasks/full_import_spec.rb new file mode 100644 index 000000000..f263f15fc --- /dev/null +++ b/spec/lib/tasks/full_import_spec.rb @@ -0,0 +1,44 @@ +require "rails_helper" +require "rake" + +describe "full import", type: :task do + let(:instance_name) { "paas_import_instance" } + let(:paas_config_service) { instance_double(Configuration::PaasConfigurationService) } + let(:storage_service) { instance_double(Storage::S3Service) } + let(:orgs_list) { "Institution name,Id,Old Completed lettings logs,Old In progress lettings logs,Old Completed sales logs,Old In progress sales logs\norg1,1.zip,0,0,0,0\norg2,2.zip,0,0,0,0" } + + before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:write_file).and_return(nil) + allow(storage_service).to receive(:get_file_io).and_return(orgs_list) + allow(Configuration::PaasConfigurationService).to receive(:new).and_return(paas_config_service) + allow(ENV).to receive(:[]) + allow(ENV).to receive(:[]).with("IMPORT_PAAS_INSTANCE").and_return(instance_name) + end + + describe "import:generate_reports" do + subject(:task) { Rake::Task["import:generate_reports"] } + + before do + Rake.application.rake_require("tasks/full_import") + Rake::Task.define_task(:environment) + task.reenable + end + + context "when generating report" do + let(:import_report_service) { instance_double(Imports::ImportReportService) } + + before do + allow(Imports::ImportReportService).to receive(:new).and_return(import_report_service) + end + + it "creates a report using given organisation csv" do + expect(Storage::S3Service).to receive(:new).with(paas_config_service, instance_name) + expect(Imports::ImportReportService).to receive(:new).with(storage_service, CSV.parse(orgs_list, headers: true)) + expect(import_report_service).to receive(:create_reports).with("some_name") + + task.invoke("some_name") + end + end + end +end diff --git a/spec/lib/tasks/merge_organisations_spec.rb b/spec/lib/tasks/merge_organisations_spec.rb new file mode 100644 index 000000000..4155a4389 --- /dev/null +++ b/spec/lib/tasks/merge_organisations_spec.rb @@ -0,0 +1,41 @@ +require "rails_helper" +require "rake" + +RSpec.describe "emails" do + describe ":merge_organisations", type: :task do + subject(:task) { Rake::Task["merge:merge_organisations"] } + + let(:organisation) { create(:organisation) } + let(:merging_organisation) { create(:organisation) } + + let(:merge_organisations_service) { Merge::MergeOrganisationsService.new(absorbing_organisation_id: organisation.id, merging_organisation_ids: [merging_organisation.id]) } + + before do + allow(Merge::MergeOrganisationsService).to receive(:new).and_return(merge_organisations_service) + allow(merge_organisations_service).to receive(:call).and_return(nil) + Rake.application.rake_require("tasks/merge_organisations") + Rake::Task.define_task(:environment) + task.reenable + end + + context "when the rake task is run" do + it "raises an error when no parameters are given" do + expect { task.invoke(nil) }.to raise_error(RuntimeError, "Usage: rake merge:merge_organisations[absorbing_organisation_id, merging_organisation_ids]") + end + + it "raises an error when only absorbing organisation is given" do + expect { task.invoke(1, nil) }.to raise_error(RuntimeError, "Usage: rake merge:merge_organisations[absorbing_organisation_id, merging_organisation_ids]") + end + + it "raises an error when only merging organisations are given" do + expect { task.invoke(nil, "1 2") }.to raise_error(RuntimeError, "Usage: rake merge:merge_organisations[absorbing_organisation_id, merging_organisation_ids]") + end + + it "raises runs the service with correct organisation IDs" do + expect(Merge::MergeOrganisationsService).to receive(:new).with(absorbing_organisation_id: 1, merging_organisation_ids: [2, 3]).once + expect(merge_organisations_service).to receive(:call).once + task.invoke(1, "2 3") + end + end + end +end diff --git a/spec/mailers/resend_invitation_mailer_spec.rb b/spec/mailers/resend_invitation_mailer_spec.rb index 02a6189d4..a5eadad20 100644 --- a/spec/mailers/resend_invitation_mailer_spec.rb +++ b/spec/mailers/resend_invitation_mailer_spec.rb @@ -57,9 +57,31 @@ RSpec.describe ResendInvitationMailer do end it "sends a reinvitation" do - expect(notify_client).to receive(:send_email).with(email_address: "active_user@example.com", template_id: User::RECONFIRMABLE_TEMPLATE_ID, personalisation:).once + expect(notify_client).to receive(:send_email).with(email_address: "active_user@example.com", template_id: User::CONFIRMABLE_TEMPLATE_ID, personalisation:).once described_class.new.resend_invitation_email(active_user) end end + + context "with unconfirmed user after the initial invitation has been sent" do + let!(:unconfirmed_user) { create(:user, organisation:, confirmation_token: "dluch", initial_confirmation_sent: true, old_user_id: "234", sign_in_count: 0, confirmed_at: nil) } + + let(:personalisation) do + { + name: unconfirmed_user.name, + email: unconfirmed_user.email, + organisation: unconfirmed_user.organisation.name, + link: include("/account/confirmation?confirmation_token=#{unconfirmed_user.confirmation_token}"), + } + end + + before do + LegacyUser.destroy_all + end + + it "sends a reinvitation" do + expect(notify_client).to receive(:send_email).with(email_address: unconfirmed_user.email, template_id: User::RECONFIRMABLE_TEMPLATE_ID, personalisation:).once + described_class.new.resend_invitation_email(unconfirmed_user) + end + end end end diff --git a/spec/models/lettings_log_spec.rb b/spec/models/lettings_log_spec.rb index 0d0750f57..1ae394c3d 100644 --- a/spec/models/lettings_log_spec.rb +++ b/spec/models/lettings_log_spec.rb @@ -248,8 +248,13 @@ RSpec.describe LettingsLog do end it "correctly derives and saves incref" do - record_from_db = described_class.find(lettings_log.id) - expect(record_from_db["incref"]).to eq(1) + expect(lettings_log.reload.incref).to eq(1) + + lettings_log.update!(net_income_known: 1) + expect(lettings_log.reload.incref).to eq(2) + + lettings_log.update!(net_income_known: 0) + expect(lettings_log.reload.incref).to eq(0) end it "correctly derives and saves renttype" do @@ -2246,7 +2251,6 @@ RSpec.describe LettingsLog do it "returns optional fields" do expect(lettings_log.optional_fields).to eq(%w[ - first_time_property_let_as_social_housing tenancycode propcode chcharge @@ -2260,7 +2264,6 @@ RSpec.describe LettingsLog do it "returns optional fields" do expect(lettings_log.optional_fields).to eq(%w[ - first_time_property_let_as_social_housing tenancycode propcode chcharge diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb index 1fe359136..f24a5f4fa 100644 --- a/spec/models/location_spec.rb +++ b/spec/models/location_spec.rb @@ -930,6 +930,79 @@ RSpec.describe Location, type: :model do end end + describe "filter by status" do + let!(:incomplete_location) { FactoryBot.create(:location, :incomplete, startdate: Time.zone.local(2022, 4, 1)) } + let!(:active_location) { FactoryBot.create(:location, startdate: Time.zone.local(2022, 4, 1)) } + let(:deactivating_soon_location) { FactoryBot.create(:location, startdate: Time.zone.local(2022, 4, 1)) } + let(:deactivated_location) { FactoryBot.create(:location, startdate: Time.zone.local(2022, 4, 1)) } + let(:reactivating_soon_location) { FactoryBot.create(:location, startdate: Time.zone.local(2022, 4, 1)) } + let!(:activating_soon_location) { FactoryBot.create(:location, startdate: Time.zone.local(2022, 7, 7)) } + + before do + Timecop.freeze(2022, 6, 7) + FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 8, 8), location: deactivating_soon_location) + deactivating_soon_location.save! + FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 6), location: deactivated_location) + deactivated_location.save! + FactoryBot.create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 6, 7), reactivation_date: Time.zone.local(2022, 6, 8), location: reactivating_soon_location) + reactivating_soon_location.save! + end + + after do + Timecop.unfreeze + end + + context "when filtering by incomplete status" do + it "returns only incomplete locations" do + expect(described_class.filter_by_status(%w[incomplete]).count).to eq(1) + expect(described_class.filter_by_status(%w[incomplete]).first).to eq(incomplete_location) + end + end + + context "when filtering by active status" do + it "returns only active locations" do + expect(described_class.filter_by_status(%w[active]).count).to eq(1) + expect(described_class.filter_by_status(%w[active]).first).to eq(active_location) + end + end + + context "when filtering by deactivating_soon status" do + it "returns only deactivating_soon locations" do + expect(described_class.filter_by_status(%w[deactivating_soon]).count).to eq(1) + expect(described_class.filter_by_status(%w[deactivating_soon]).first).to eq(deactivating_soon_location) + end + end + + context "when filtering by deactivated status" do + it "returns only deactivated locations" do + expect(described_class.filter_by_status(%w[deactivated]).count).to eq(1) + expect(described_class.filter_by_status(%w[deactivated]).first).to eq(deactivated_location) + end + end + + context "when filtering by reactivating_soon status" do + it "returns only reactivating_soon locations" do + expect(described_class.filter_by_status(%w[reactivating_soon]).count).to eq(1) + expect(described_class.filter_by_status(%w[reactivating_soon]).first).to eq(reactivating_soon_location) + end + end + + context "when filtering by activating_soon status" do + it "returns only activating_soon locations" do + expect(described_class.filter_by_status(%w[activating_soon]).count).to eq(1) + expect(described_class.filter_by_status(%w[activating_soon]).first).to eq(activating_soon_location) + end + end + + context "when filtering by multiple statuses" do + it "returns relevant locations" do + expect(described_class.filter_by_status(%w[deactivating_soon activating_soon]).count).to eq(2) + expect(described_class.filter_by_status(%w[deactivating_soon activating_soon])).to include(activating_soon_location) + expect(described_class.filter_by_status(%w[deactivating_soon activating_soon])).to include(deactivating_soon_location) + end + end + end + describe "available_from" do context "when there is a startdate" do let(:location) { FactoryBot.build(:location, startdate: Time.zone.local(2022, 4, 6)) } diff --git a/spec/models/scheme_spec.rb b/spec/models/scheme_spec.rb index 0bdcb940a..049da077a 100644 --- a/spec/models/scheme_spec.rb +++ b/spec/models/scheme_spec.rb @@ -90,6 +90,25 @@ RSpec.describe Scheme, type: :model do end end + context "when filtering by owning organisation" do + let(:organisation_1) { create(:organisation) } + let(:organisation_2) { create(:organisation) } + let(:organisation_3) { create(:organisation) } + + before do + create(:scheme, owning_organisation: organisation_1) + create(:scheme, owning_organisation: organisation_1) + create(:scheme, owning_organisation: organisation_2) + create(:scheme, owning_organisation: organisation_2) + end + + it "filters by given owning organisation" do + expect(described_class.filter_by_owning_organisation([organisation_1]).count).to eq(2) + expect(described_class.filter_by_owning_organisation([organisation_1, organisation_2]).count).to eq(4) + expect(described_class.filter_by_owning_organisation([organisation_3]).count).to eq(0) + end + end + context "when filtering by status" do let!(:incomplete_scheme) { FactoryBot.create(:scheme, :incomplete) } let(:active_scheme) { FactoryBot.create(:scheme) } diff --git a/spec/requests/lettings_logs_controller_spec.rb b/spec/requests/lettings_logs_controller_spec.rb index 4e4cb96c2..6cf53acc9 100644 --- a/spec/requests/lettings_logs_controller_spec.rb +++ b/spec/requests/lettings_logs_controller_spec.rb @@ -562,6 +562,13 @@ RSpec.describe LettingsLogsController, type: :request do expect(response).to redirect_to(resume_bulk_upload_lettings_result_path(bulk_upload)) end + + it "allows returning to all logs" do + get "/lettings-logs?bulk_upload_id[]=#{bulk_upload.id}" + + follow_redirect! + expect(page).to have_link("Return to lettings logs", href: clear_filters_path(filter_type: "lettings_logs")) + end end end diff --git a/spec/requests/locations_controller_spec.rb b/spec/requests/locations_controller_spec.rb index 52b1c087f..7dccbec1b 100644 --- a/spec/requests/locations_controller_spec.rb +++ b/spec/requests/locations_controller_spec.rb @@ -119,6 +119,54 @@ RSpec.describe LocationsController, type: :request do it "returns 200" do expect(response).to be_successful end + + context "when filtering" do + context "with status filter" do + let(:scheme) { create(:scheme, owning_organisation: user.organisation) } + let!(:incomplete_location) { create(:location, :incomplete, scheme:, startdate: Time.zone.local(2022, 4, 1)) } + let!(:active_location) { create(:location, scheme:, startdate: Time.zone.local(2022, 4, 1)) } + let!(:deactivated_location) { create(:location, scheme:, startdate: Time.zone.local(2022, 4, 1)) } + + before do + create(:location_deactivation_period, deactivation_date: Time.zone.local(2022, 4, 1), location: deactivated_location) + end + + it "shows locations for multiple selected statuses" do + get "/schemes/#{scheme.id}/locations?status[]=incomplete&status[]=active", headers:, params: {} + expect(page).to have_link(incomplete_location.postcode) + expect(page).to have_link(active_location.postcode) + end + + it "shows filtered incomplete locations" do + get "/schemes/#{scheme.id}/locations?status[]=incomplete", headers:, params: {} + expect(page).to have_link(incomplete_location.postcode) + expect(page).not_to have_link(active_location.postcode) + end + + it "shows filtered active locations" do + get "/schemes/#{scheme.id}/locations?status[]=active", headers:, params: {} + expect(page).to have_link(active_location.postcode) + expect(page).not_to have_link(incomplete_location.postcode) + end + + it "shows filtered deactivated locations" do + get "/schemes/#{scheme.id}/locations?status[]=deactivated", headers:, params: {} + expect(page).to have_link(deactivated_location.postcode) + expect(page).not_to have_link(active_location.postcode) + expect(page).not_to have_link(incomplete_location.postcode) + end + + it "does not reset the filters" do + get "/schemes/#{scheme.id}/locations?status[]=incomplete", headers:, params: {} + expect(page).to have_link(incomplete_location.postcode) + expect(page).not_to have_link(active_location.postcode) + + get "/schemes/#{scheme.id}/locations", headers:, params: {} + expect(page).to have_link(incomplete_location.postcode) + expect(page).not_to have_link(active_location.postcode) + end + end + end end context "when signed in as a data coordinator user" do diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb index 896f760ba..bacb5ca33 100644 --- a/spec/requests/organisations_controller_spec.rb +++ b/spec/requests/organisations_controller_spec.rb @@ -268,8 +268,8 @@ RSpec.describe OrganisationsController, type: :request do end it "displays a link to merge organisations" do - expect(page).to have_content("Is your organisation merging with another?") - expect(page).to have_link("Let us know using this form", href: "/organisations/#{organisation.id}/merge-request") + expect(page).to have_content("To report a merge or update your organisation details, ") + expect(page).to have_link("contact the helpdesk", href: "https://dluhcdigital.atlassian.net/servicedesk/customer/portal/6/group/11") end end diff --git a/spec/requests/sales_logs_controller_spec.rb b/spec/requests/sales_logs_controller_spec.rb index 5d9819d55..abe10af8e 100644 --- a/spec/requests/sales_logs_controller_spec.rb +++ b/spec/requests/sales_logs_controller_spec.rb @@ -440,6 +440,13 @@ RSpec.describe SalesLogsController, type: :request do expect(response).to redirect_to(resume_bulk_upload_sales_result_path(bulk_upload)) end + + it "allows returning to all logs" do + get "/sales-logs?bulk_upload_id[]=#{bulk_upload.id}" + + follow_redirect! + expect(page).to have_link("Return to sales logs", href: clear_filters_path(filter_type: "sales_logs")) + end end end diff --git a/spec/requests/schemes_controller_spec.rb b/spec/requests/schemes_controller_spec.rb index b145d7288..50d83a5b3 100644 --- a/spec/requests/schemes_controller_spec.rb +++ b/spec/requests/schemes_controller_spec.rb @@ -57,13 +57,13 @@ RSpec.describe SchemesController, type: :request do end context "when parent organisation has schemes" do - let(:parent_organisation) { FactoryBot.create(:organisation) } - let!(:parent_schemes) { FactoryBot.create_list(:scheme, 5, owning_organisation: parent_organisation) } + let(:parent_organisation) { create(:organisation) } + let!(:parent_schemes) { create_list(:scheme, 5, owning_organisation: parent_organisation) } before do create(:organisation_relationship, parent_organisation:, child_organisation: user.organisation) parent_schemes.each do |scheme| - FactoryBot.create(:location, scheme:) + create(:location, scheme:) end get "/schemes" end @@ -77,6 +77,48 @@ RSpec.describe SchemesController, type: :request do end context "when filtering" do + context "with owning organisation filter" do + context "when user org does not have owning orgs" do + it "does not show filter" do + expect(page).not_to have_content("Owned by") + end + end + + context "when user org has owning orgs" do + let!(:organisation1) { create(:organisation) } + let!(:scheme1) { create(:scheme, owning_organisation: organisation1) } + let!(:scheme2) { create(:scheme, owning_organisation: user.organisation) } + + before do + org = user.organisation + org.stock_owners = [organisation1, user.organisation] + org.save! + end + + context "when filtering by all owning orgs" do + it "shows schemes for all owning orgs" do + get "/schemes?owning_organisation_select=all", headers:, params: {} + follow_redirect! + + expect(page).to have_content("Owned by") + expect(page).to have_link(scheme1.service_name) + expect(page).to have_link(scheme2.service_name) + end + end + + context "when filtering by an owning org" do + it "when filtering by an owning org" do + get "/schemes?owning_organisation=#{organisation1.id}", headers:, params: {} + follow_redirect! + + expect(page).to have_content("Owned by") + expect(page).to have_link(scheme1.service_name) + expect(page).not_to have_link(scheme2.service_name) + end + end + end + end + context "with status filter" do let!(:incomplete_scheme) { create(:scheme, :incomplete, owning_organisation: user.organisation) } let(:active_scheme) { create(:scheme, owning_organisation: user.organisation) } @@ -282,6 +324,40 @@ RSpec.describe SchemesController, type: :request do end context "when filtering" do + context "with owning organisation filter" do + context "when user org does not have owning orgs" do + it "shows the filter" do + expect(page).to have_content("Owned by") + end + end + + context "when user org has owning orgs" do + let!(:organisation1) { create(:organisation) } + let!(:scheme1) { create(:scheme, owning_organisation: organisation1) } + let!(:scheme2) { create(:scheme, owning_organisation: user.organisation) } + + context "when filtering by all owning orgs" do + it "shows schemes for all owning orgs" do + get "/schemes?owning_organisation_select=all", headers:, params: {} + + expect(page).to have_content("Owned by") + expect(page).to have_link(scheme1.service_name) + expect(page).to have_link(scheme2.service_name) + end + end + + context "when filtering by an owning org" do + it "when filtering by an owning org" do + get "/schemes?owning_organisation=#{organisation1.id}", headers:, params: {} + + expect(page).to have_content("Owned by") + expect(page).to have_link(scheme1.service_name) + expect(page).not_to have_link(scheme2.service_name) + end + end + end + end + context "with status filter" do let!(:incomplete_scheme) { create(:scheme, :incomplete) } let(:active_scheme) { create(:scheme) } @@ -457,11 +533,11 @@ RSpec.describe SchemesController, type: :request do end context "when coordinator attempts to see scheme belonging to a parent organisation" do - let(:parent_organisation) { FactoryBot.create(:organisation) } - let!(:specific_scheme) { FactoryBot.create(:scheme, owning_organisation: parent_organisation) } + let(:parent_organisation) { create(:organisation) } + let!(:specific_scheme) { create(:scheme, owning_organisation: parent_organisation) } before do - FactoryBot.create(:location, scheme: specific_scheme) + create(:location, scheme: specific_scheme) create(:organisation_relationship, parent_organisation:, child_organisation: user.organisation) get "/schemes/#{specific_scheme.id}" end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 984090863..ec2e0dde8 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -1538,7 +1538,7 @@ RSpec.describe UsersController, type: :request do expect(whodunnit_actor.id).to eq(user.id) end - context "when user changes email, dpo and key contact" do + context "when user changes email, dpo and key contact", :aggregate_failures do let(:params) { { id: user.id, user: { name: new_name, email: new_email, is_dpo: "true", is_key_contact: "true" } } } let(:personalisation) do { @@ -1551,6 +1551,8 @@ RSpec.describe UsersController, type: :request do before do user.legacy_users.destroy_all + + allow(FeatureToggle).to receive(:new_email_journey?).and_return(false) end it "allows changing email and dpo" do @@ -1566,6 +1568,43 @@ RSpec.describe UsersController, type: :request do expect(notify_client).to receive(:send_email).with(email_address: user.email, template_id: User::CONFIRMABLE_TEMPLATE_ID, personalisation:).once request end + + context "with new email journy enabled" do + before do + allow(FeatureToggle).to receive(:new_email_journey?).and_return(true) + end + + it "shows flash notice" do + patch("/users/#{other_user.id}", headers:, params:) + + expect(flash[:notice]).to eq("An email has been sent to #{new_email} to confirm this change.") + end + + it "sends new flow emails" do + expect(notify_client).to receive(:send_email).with( + email_address: other_user.email, + template_id: User::FOR_OLD_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID, + personalisation: { + new_email:, + old_email: other_user.email, + }, + ).once + + expect(notify_client).to receive(:send_email).with( + email_address: new_email, + template_id: User::FOR_NEW_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID, + personalisation: { + new_email:, + old_email: other_user.email, + link: include("/account/confirmation?confirmation_token="), + }, + ).once + + expect(notify_client).not_to receive(:send_email) + + patch "/users/#{other_user.id}", headers:, params: + end + end end context "when we update the user password" do @@ -1679,13 +1718,28 @@ RSpec.describe UsersController, type: :request do expect(page).to have_content(other_user.reload.email.to_s) end - context "when the support user tries to update the user’s password" do + context "when the support user tries to update the user’s password", :aggregate_failures do let(:params) do { - id: user.id, user: { password: new_name, password_confirmation: new_name, name: "new name" } + id: user.id, user: { password: new_name, password_confirmation: new_name, name: "new name", email: new_email } } end + let(:personalisation) do + { + name: params[:user][:name], + email: new_email, + organisation: other_user.organisation.name, + link: include("/account/confirmation?confirmation_token="), + } + end + + before do + other_user.legacy_users.destroy_all + + allow(FeatureToggle).to receive(:new_email_journey?).and_return(false) + end + it "does not update the password" do expect { patch "/users/#{other_user.id}", headers:, params: } .not_to change(other_user, :encrypted_password) @@ -1695,6 +1749,57 @@ RSpec.describe UsersController, type: :request do expect { patch "/users/#{other_user.id}", headers:, params: } .to change { other_user.reload.name }.from("Danny Rojas").to("new name") end + + it "allows changing email" do + expect { patch "/users/#{other_user.id}", headers:, params: } + .to change { other_user.reload.unconfirmed_email }.from(nil).to(new_email) + end + + it "sends a confirmation email to both emails" do + expect(notify_client).to receive(:send_email).with(email_address: other_user.email, template_id: User::CONFIRMABLE_TEMPLATE_ID, personalisation:).once + expect(notify_client).to receive(:send_email).with(email_address: new_email, template_id: User::CONFIRMABLE_TEMPLATE_ID, personalisation:).once + + expect(notify_client).not_to receive(:send_email) + + patch "/users/#{other_user.id}", headers:, params: + end + + context "with new user email flow enabled" do + before do + allow(FeatureToggle).to receive(:new_email_journey?).and_return(true) + end + + it "shows flash notice" do + patch("/users/#{other_user.id}", headers:, params:) + + expect(flash[:notice]).to eq("An email has been sent to #{new_email} to confirm this change.") + end + + it "sends new flow emails" do + expect(notify_client).to receive(:send_email).with( + email_address: other_user.email, + template_id: User::FOR_OLD_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID, + personalisation: { + new_email:, + old_email: other_user.email, + }, + ).once + + expect(notify_client).to receive(:send_email).with( + email_address: new_email, + template_id: User::FOR_NEW_EMAIL_CHANGED_BY_OTHER_USER_TEMPLATE_ID, + personalisation: { + new_email:, + old_email: other_user.email, + link: include("/account/confirmation?confirmation_token="), + }, + ).once + + expect(notify_client).not_to receive(:send_email) + + patch "/users/#{other_user.id}", headers:, params: + end + end end end end diff --git a/spec/services/bulk_upload/lettings/year2022/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2022/row_parser_spec.rb index b91caa969..6b5eaa2ae 100644 --- a/spec/services/bulk_upload/lettings/year2022/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2022/row_parser_spec.rb @@ -1914,6 +1914,16 @@ RSpec.describe BulkUpload::Lettings::Year2022::RowParser do expect(parser.log.housingneeds_other).to eq(1) end end + + context "when field_58 is nil and one housingneeds option is selected" do + let(:attributes) { { bulk_upload:, field_58: nil, field_55: "1" } } + + it "sets to 0" do + expect(parser.errors[:field_58]).to be_blank + expect(parser.errors[:field_55]).to be_blank + expect(parser.log.housingneeds_other).to eq(0) + end + end end end diff --git a/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb b/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb index e12785738..cac166f46 100644 --- a/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb +++ b/spec/services/bulk_upload/lettings/year2023/row_parser_spec.rb @@ -754,6 +754,16 @@ RSpec.describe BulkUpload::Lettings::Year2023::RowParser do expect(parser.errors[:field_87]).to be_present end end + + context "when one item selected and field_86 is blank" do + let(:attributes) { setup_section_params.merge({ field_83: "1", field_86: nil }) } + + it "sets other disabled access needs as no" do + expect(parser.errors[:field_83]).to be_blank + expect(parser.errors[:field_86]).to be_blank + expect(parser.log.housingneeds_other).to eq(0) + end + end end describe "#field_89, field_98 - 99" do diff --git a/spec/services/imports/import_report_service_spec.rb b/spec/services/imports/import_report_service_spec.rb new file mode 100644 index 000000000..1d241754d --- /dev/null +++ b/spec/services/imports/import_report_service_spec.rb @@ -0,0 +1,70 @@ +require "rails_helper" + +RSpec.describe Imports::ImportReportService do + subject(:report_service) { described_class.new(storage_service, institutions_csv) } + + let(:storage_service) { instance_double(Storage::S3Service) } + + describe "#generate_missing_data_coordinators_report" do + context "when all organisations have data coordinators" do + let!(:organisation) { create(:organisation, old_visible_id: "1", name: "org1") } + let(:institutions_csv) { CSV.parse("Institution name,Id,Old Completed lettings logs,Old In progress lettings logs,Old Completed sales logs,Old In progress sales logs\norg1,1,2,1,4,3", headers: true) } + + before do + create(:user, :data_coordinator, organisation:) + end + + it "writes an empty organisations without a data coordinators report" do + expect(storage_service).to receive(:write_file).with("OrganisationsWithoutDataCoordinators_report_suffix.csv", "\uFEFFOrganisation ID,Old Organisation ID,Organisation Name\n") + + report_service.generate_missing_data_coordinators_report("report_suffix.csv") + end + end + + context "when some organisations have no data coordinators" do + let!(:organisation) { create(:organisation, old_visible_id: "1", name: "org1") } + let!(:organisation2) { create(:organisation, old_visible_id: "2", name: "org2") } + let!(:organisation3) { create(:organisation, old_visible_id: "3", name: "org3") } + let(:institutions_csv) { CSV.parse("Institution name,Id,Old Completed lettings logs,Old In progress lettings logs,Old Completed sales logs,Old In progress sales logs\norg1,1,2,1,4,3\norg2,2,5,6,5,7\norg3,3,5,6,5,7", headers: true) } + + before do + create(:user, :data_coordinator, organisation:) + end + + it "writes an empty organisations without a data coordinators report" do + expect(storage_service).to receive(:write_file).with("OrganisationsWithoutDataCoordinators_report_suffix.csv", "\uFEFFOrganisation ID,Old Organisation ID,Organisation Name\n#{organisation2.id},2,org2\n#{organisation3.id},3,org3\n") + + report_service.generate_missing_data_coordinators_report("report_suffix.csv") + end + end + + context "when organisation has an inactive data coordinator" do + let!(:organisation) { create(:organisation, old_visible_id: "1", name: "org1") } + let(:institutions_csv) { CSV.parse("Institution name,Id,Old Completed lettings logs,Old In progress lettings logs,Old Completed sales logs,Old In progress sales logs\norg1,1,2,1,4,3", headers: true) } + + before do + create(:user, :data_coordinator, organisation:, active: false) + end + + it "includes that organisation in the data coordinators report" do + expect(storage_service).to receive(:write_file).with("OrganisationsWithoutDataCoordinators_report_suffix.csv", "\uFEFFOrganisation ID,Old Organisation ID,Organisation Name\n#{organisation.id},1,org1\n") + + report_service.generate_missing_data_coordinators_report("report_suffix.csv") + end + end + end + + describe "#generate_logs_report" do + let(:institutions_csv) { CSV.parse("Institution name,Id,Old Completed lettings logs,Old In progress lettings logs,Old Completed sales logs,Old In progress sales logs\norg1,1,2,1,4,3\norg2,2,5,6,5,7", headers: true) } + + before do + create(:organisation, old_visible_id: "1", name: "org1") + create(:organisation, old_visible_id: "2", name: "org2") + end + + it "generates a report with imported logs" do + expect(storage_service).to receive(:write_file).with("MigratedLogsReport_report_suffix.csv", "\uFEFFInstitution name,Id,Old Completed lettings logs,Old In progress lettings logs,Old Completed sales logs,Old In progress sales logs,New Completed lettings logs,New In Progress lettings logs,New Completed sales logs,New In Progress sales logs\norg1,1,2,1,4,3,0,0,0,0\norg2,2,5,6,5,7,0,0,0,0\n") + report_service.generate_logs_report("report_suffix.csv") + end + end +end diff --git a/spec/services/merge/merge_organisations_service_spec.rb b/spec/services/merge/merge_organisations_service_spec.rb new file mode 100644 index 000000000..2c2af0f3e --- /dev/null +++ b/spec/services/merge/merge_organisations_service_spec.rb @@ -0,0 +1,323 @@ +require "rails_helper" + +RSpec.describe Merge::MergeOrganisationsService do + subject(:merge_organisations_service) { described_class.new(absorbing_organisation_id: absorbing_organisation.id, merging_organisation_ids: [merging_organisation_ids]) } + + let(:absorbing_organisation) { create(:organisation, holds_own_stock: false) } + let(:absorbing_organisation_user) { create(:user, organisation: absorbing_organisation) } + + describe "#call" do + context "when merging a single organisation into an existing organisation" do + let(:merging_organisation) { create(:organisation, holds_own_stock: true, name: "fake org") } + + let(:merging_organisation_ids) { [merging_organisation.id] } + let!(:merging_organisation_user) { create(:user, organisation: merging_organisation, name: "fake name", email: "fake@email.com") } + + it "moves the users from merging organisation to absorbing organisation" do + expect(Rails.logger).to receive(:info).with("Merged users from fake org:") + expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{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:") + merge_organisations_service.call + + merging_organisation_user.reload + expect(merging_organisation_user.organisation).to eq(absorbing_organisation) + end + + it "sets merge date on merged organisation" do + merge_organisations_service.call + + merging_organisation.reload + expect(merging_organisation.merge_date.to_date).to eq(Time.zone.today) + expect(merging_organisation.absorbing_organisation_id).to eq(absorbing_organisation.id) + end + + it "combines organisation data" do + merge_organisations_service.call + + absorbing_organisation.reload + expect(absorbing_organisation.holds_own_stock).to eq(true) + end + + it "rolls back if there's an error" do + allow(Organisation).to receive(:find).with([merging_organisation_ids]).and_return(Organisation.find(merging_organisation_ids)) + allow(Organisation).to receive(:find).with(absorbing_organisation.id).and_return(absorbing_organisation) + allow(absorbing_organisation).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) + expect(Rails.logger).to receive(:error).with("Organisation merge failed with: Record invalid") + merge_organisations_service.call + + absorbing_organisation.reload + merging_organisation.reload + expect(absorbing_organisation.holds_own_stock).to eq(false) + expect(merging_organisation.merge_date).to eq(nil) + expect(merging_organisation.absorbing_organisation_id).to eq(nil) + expect(merging_organisation_user.organisation).to eq(merging_organisation) + end + + context "and merging organisation rent periods" do + before do + OrganisationRentPeriod.create!(organisation: absorbing_organisation, rent_period: 1) + OrganisationRentPeriod.create!(organisation: absorbing_organisation, rent_period: 3) + OrganisationRentPeriod.create!(organisation: merging_organisation, rent_period: 1) + OrganisationRentPeriod.create!(organisation: merging_organisation, rent_period: 2) + end + + it "combines organisation rent periods" do + expect(absorbing_organisation.rent_periods.count).to eq(2) + merge_organisations_service.call + + absorbing_organisation.reload + expect(absorbing_organisation.rent_periods.count).to eq(3) + expect(absorbing_organisation.rent_periods).to include(1) + expect(absorbing_organisation.rent_periods).to include(2) + expect(absorbing_organisation.rent_periods).to include(3) + end + + it "rolls back if there's an error" do + allow(Organisation).to receive(:find).with([merging_organisation_ids]).and_return(Organisation.find(merging_organisation_ids)) + allow(Organisation).to receive(:find).with(absorbing_organisation.id).and_return(absorbing_organisation) + allow(absorbing_organisation).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) + expect(Rails.logger).to receive(:error).with("Organisation merge failed with: Record invalid") + merge_organisations_service.call + + absorbing_organisation.reload + merging_organisation.reload + expect(absorbing_organisation.rent_periods.count).to eq(2) + expect(merging_organisation.rent_periods.count).to eq(2) + end + end + + context "and merging organisation relationships" do + let(:other_organisation) { create(:organisation) } + let!(:merging_organisation_relationship) { create(:organisation_relationship, parent_organisation: merging_organisation) } + let!(:absorbing_organisation_relationship) { create(:organisation_relationship, parent_organisation: absorbing_organisation) } + + before do + create(:organisation_relationship, parent_organisation: absorbing_organisation, child_organisation: merging_organisation) + create(:organisation_relationship, parent_organisation: merging_organisation, child_organisation: other_organisation) + create(:organisation_relationship, parent_organisation: absorbing_organisation, child_organisation: other_organisation) + end + + it "combines organisation relationships" do + merge_organisations_service.call + + absorbing_organisation.reload + expect(absorbing_organisation.child_organisations).to include(other_organisation) + expect(absorbing_organisation.child_organisations).to include(absorbing_organisation_relationship.child_organisation) + expect(absorbing_organisation.child_organisations).to include(merging_organisation_relationship.child_organisation) + expect(absorbing_organisation.child_organisations).not_to include(merging_organisation) + expect(absorbing_organisation.parent_organisations.count).to eq(0) + expect(absorbing_organisation.child_organisations.count).to eq(3) + end + + it "rolls back if there's an error" do + allow(Organisation).to receive(:find).with([merging_organisation_ids]).and_return(Organisation.find(merging_organisation_ids)) + allow(Organisation).to receive(:find).with(absorbing_organisation.id).and_return(absorbing_organisation) + allow(absorbing_organisation).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) + expect(Rails.logger).to receive(:error).with("Organisation merge failed with: Record invalid") + merge_organisations_service.call + + absorbing_organisation.reload + expect(absorbing_organisation.child_organisations.count).to eq(3) + expect(absorbing_organisation.child_organisations).to include(other_organisation) + expect(absorbing_organisation.child_organisations).to include(merging_organisation) + expect(absorbing_organisation.child_organisations).to include(absorbing_organisation_relationship.child_organisation) + end + end + + context "and merging organisation schemes and locations" do + let!(:scheme) { create(:scheme, owning_organisation: merging_organisation) } + let!(:location) { create(:location, scheme:) } + let!(:deactivated_location) { create(:location, scheme:) } + let!(:deactivated_scheme) { create(:scheme, owning_organisation: merging_organisation) } + let!(:owned_lettings_log) { create(:lettings_log, :sh, scheme:, location:, startdate: Time.zone.tomorrow, owning_organisation: merging_organisation) } + let!(:owned_lettings_log_no_location) { create(:lettings_log, :sh, scheme:, startdate: Time.zone.tomorrow, owning_organisation: merging_organisation) } + + before do + create(:location, scheme:, name: "fake location", postcode: "A1 1AA") + create(:location, scheme: deactivated_scheme) + create(:scheme_deactivation_period, scheme: deactivated_scheme, deactivation_date: Time.zone.today - 1.month) + create(:location_deactivation_period, location: deactivated_location, deactivation_date: Time.zone.today - 1.month) + create(:lettings_log, scheme:, location:, startdate: Time.zone.yesterday) + create(:lettings_log, startdate: Time.zone.tomorrow, managing_organisation: merging_organisation) + end + + it "combines organisation schemes and locations" do + expect(Rails.logger).to receive(:info).with("Merged users from fake org:") + expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{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(Rails.logger).to receive(:info).with(/\t#{scheme.service_name} \(S/) + merge_organisations_service.call + + absorbing_organisation.reload + expect(absorbing_organisation.owned_schemes.count).to eq(1) + expect(absorbing_organisation.owned_schemes.first.service_name).to eq(scheme.service_name) + expect(absorbing_organisation.owned_schemes.first.locations.count).to eq(2) + expect(absorbing_organisation.owned_schemes.first.locations.first.postcode).to eq(location.postcode) + expect(scheme.scheme_deactivation_periods.count).to eq(1) + expect(scheme.scheme_deactivation_periods.first.deactivation_date.to_date).to eq(Time.zone.today) + end + + it "moves relevant logs and assigns the new scheme" do + merge_organisations_service.call + + absorbing_organisation.reload + merging_organisation.reload + expect(absorbing_organisation.owned_lettings_logs.count).to eq(2) + expect(absorbing_organisation.managed_lettings_logs.count).to eq(1) + expect(absorbing_organisation.owned_lettings_logs.find(owned_lettings_log.id).scheme).to eq(absorbing_organisation.owned_schemes.first) + expect(absorbing_organisation.owned_lettings_logs.find(owned_lettings_log.id).location).to eq(absorbing_organisation.owned_schemes.first.locations.first) + expect(absorbing_organisation.owned_lettings_logs.find(owned_lettings_log_no_location.id).scheme).to eq(absorbing_organisation.owned_schemes.first) + expect(absorbing_organisation.owned_lettings_logs.find(owned_lettings_log_no_location.id).location).to eq(nil) + end + + it "rolls back if there's an error" do + allow(Organisation).to receive(:find).with([merging_organisation_ids]).and_return(Organisation.find(merging_organisation_ids)) + allow(Organisation).to receive(:find).with(absorbing_organisation.id).and_return(absorbing_organisation) + allow(absorbing_organisation).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) + expect(Rails.logger).to receive(:error).with("Organisation merge failed with: Record invalid") + merge_organisations_service.call + + absorbing_organisation.reload + merging_organisation.reload + expect(absorbing_organisation.owned_schemes.count).to eq(0) + expect(scheme.scheme_deactivation_periods.count).to eq(0) + expect(owned_lettings_log.owning_organisation).to eq(merging_organisation) + expect(owned_lettings_log_no_location.owning_organisation).to eq(merging_organisation) + end + end + + context "and merging sales logs" do + let!(:sales_log) { create(:sales_log, saledate: Time.zone.tomorrow, owning_organisation: merging_organisation) } + + before do + create(:sales_log, saledate: Time.zone.yesterday, owning_organisation: merging_organisation) + end + + it "moves relevant logs" do + merge_organisations_service.call + + absorbing_organisation.reload + expect(SalesLog.filter_by_owning_organisation(absorbing_organisation).count).to eq(1) + expect(SalesLog.filter_by_owning_organisation(absorbing_organisation).first).to eq(sales_log) + end + + it "rolls back if there's an error" do + allow(Organisation).to receive(:find).with([merging_organisation_ids]).and_return(Organisation.find(merging_organisation_ids)) + allow(Organisation).to receive(:find).with(absorbing_organisation.id).and_return(absorbing_organisation) + allow(absorbing_organisation).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) + expect(Rails.logger).to receive(:error).with("Organisation merge failed with: Record invalid") + merge_organisations_service.call + + absorbing_organisation.reload + expect(absorbing_organisation.sales_logs.count).to eq(0) + expect(sales_log.owning_organisation).to eq(merging_organisation) + end + end + end + + context "when merging a multiple organisations into an existing organisation" do + let(:merging_organisation) { create(:organisation, holds_own_stock: true, name: "fake org") } + let(:merging_organisation_too) { create(:organisation, holds_own_stock: true, name: "second org") } + + let(:merging_organisation_ids) { [merging_organisation.id, merging_organisation_too.id] } + let!(:merging_organisation_user) { create(:user, organisation: merging_organisation, name: "fake name", email: "fake@email.com") } + + before do + create_list(:user, 5, organisation: merging_organisation_too) + end + + it "moves the users from merging organisations to absorbing organisation" do + expect(Rails.logger).to receive(:info).with("Merged users from fake org:") + expect(Rails.logger).to receive(:info).with("\tDanny Rojas (#{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("Merged users from second org:") + expect(Rails.logger).to receive(:info).with(/\tDanny Rojas/).exactly(6).times + expect(Rails.logger).to receive(:info).with("New schemes from fake org:") + expect(Rails.logger).to receive(:info).with("New schemes from second org:") + merge_organisations_service.call + + merging_organisation_user.reload + expect(merging_organisation_user.organisation).to eq(absorbing_organisation) + end + + it "sets merge date and absorbing organisation on merged organisations" do + merge_organisations_service.call + + merging_organisation.reload + merging_organisation_too.reload + expect(merging_organisation.merge_date.to_date).to eq(Time.zone.today) + expect(merging_organisation.absorbing_organisation_id).to eq(absorbing_organisation.id) + expect(merging_organisation_too.merge_date.to_date).to eq(Time.zone.today) + expect(merging_organisation_too.absorbing_organisation_id).to eq(absorbing_organisation.id) + end + + it "combines organisation data" do + merge_organisations_service.call + + absorbing_organisation.reload + expect(absorbing_organisation.holds_own_stock).to eq(true) + end + + it "rolls back if there's an error" do + allow(Organisation).to receive(:find).with([merging_organisation_ids]).and_return(Organisation.find(merging_organisation_ids)) + allow(Organisation).to receive(:find).with(absorbing_organisation.id).and_return(absorbing_organisation) + allow(absorbing_organisation).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) + expect(Rails.logger).to receive(:error).with("Organisation merge failed with: Record invalid") + merge_organisations_service.call + + absorbing_organisation.reload + merging_organisation.reload + expect(absorbing_organisation.holds_own_stock).to eq(false) + expect(merging_organisation.merge_date).to eq(nil) + expect(merging_organisation.absorbing_organisation_id).to eq(nil) + expect(merging_organisation_user.organisation).to eq(merging_organisation) + end + + context "and merging organisation relationships" do + let(:other_organisation) { create(:organisation) } + let!(:merging_organisation_relationship) { create(:organisation_relationship, parent_organisation: merging_organisation) } + let!(:absorbing_organisation_relationship) { create(:organisation_relationship, parent_organisation: absorbing_organisation) } + + before do + create(:organisation_relationship, parent_organisation: merging_organisation, child_organisation: absorbing_organisation) + create(:organisation_relationship, parent_organisation: merging_organisation, child_organisation: other_organisation) + create(:organisation_relationship, parent_organisation: absorbing_organisation, child_organisation: other_organisation) + create(:organisation_relationship, parent_organisation: merging_organisation, child_organisation: merging_organisation_too) + end + + it "combines organisation relationships" do + merge_organisations_service.call + + absorbing_organisation.reload + expect(absorbing_organisation.child_organisations).to include(other_organisation) + expect(absorbing_organisation.child_organisations).to include(absorbing_organisation_relationship.child_organisation) + expect(absorbing_organisation.child_organisations).to include(merging_organisation_relationship.child_organisation) + expect(absorbing_organisation.child_organisations).not_to include(merging_organisation) + expect(absorbing_organisation.parent_organisations).not_to include(merging_organisation) + expect(absorbing_organisation.child_organisations).not_to include(merging_organisation_too) + expect(absorbing_organisation.parent_organisations).not_to include(merging_organisation_too) + expect(absorbing_organisation.parent_organisations.count).to eq(0) + expect(absorbing_organisation.child_organisations.count).to eq(3) + end + + it "rolls back if there's an error" do + allow(Organisation).to receive(:find).with([merging_organisation_ids]).and_return(Organisation.find(merging_organisation_ids)) + allow(Organisation).to receive(:find).with(absorbing_organisation.id).and_return(absorbing_organisation) + allow(absorbing_organisation).to receive(:save!).and_raise(ActiveRecord::RecordInvalid) + expect(Rails.logger).to receive(:error).with("Organisation merge failed with: Record invalid") + merge_organisations_service.call + + absorbing_organisation.reload + merging_organisation.reload + expect(absorbing_organisation.child_organisations.count).to eq(2) + expect(absorbing_organisation.parent_organisations.count).to eq(1) + expect(absorbing_organisation.child_organisations).to include(other_organisation) + expect(absorbing_organisation.parent_organisations).to include(merging_organisation) + expect(absorbing_organisation.child_organisations).to include(absorbing_organisation_relationship.child_organisation) + end + end + end + end +end diff --git a/yarn.lock b/yarn.lock index dddaec069..8e34a628c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5856,9 +5856,9 @@ wildcard@^2.0.0: integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wrap-ansi@^7.0.0: version "7.0.0"