From 27188ad3a2d4bf5512ac530faefff152c7171297 Mon Sep 17 00:00:00 2001 From: Manny Dinssa <44172848+Dinssa@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:20:08 +0100 Subject: [PATCH] CLDC-3001 Handle locations and logs on deactivate schemes (#2605) --- app/controllers/schemes_controller.rb | 19 +++-- app/helpers/deactivate_confirm_helper.rb | 8 +++ app/helpers/locations_helper.rb | 31 ++++++-- app/helpers/schemes_helper.rb | 11 ++- app/models/location.rb | 70 ++++++++++++++----- app/models/scheme.rb | 38 +++++++--- app/models/scheme_deactivation_period.rb | 1 + app/models/validations/shared_validations.rb | 6 +- app/views/locations/index.html.erb | 13 +++- app/views/schemes/deactivate_confirm.html.erb | 41 ++++++++--- app/views/schemes/toggle_active.html.erb | 13 ++-- spec/fixtures/files/locations_csv_export.csv | 2 +- .../schemes_and_locations_csv_export.csv | 2 +- spec/models/location_spec.rb | 33 +++++++++ spec/requests/schemes_controller_spec.rb | 31 +++++--- .../deactivate_confirm.html.erb_spec.rb | 46 ++++++++++++ 16 files changed, 293 insertions(+), 72 deletions(-) create mode 100644 app/helpers/deactivate_confirm_helper.rb create mode 100644 spec/views/schemes/deactivate_confirm.html.erb_spec.rb diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index c0a36b920..1468bc013 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -51,17 +51,24 @@ class SchemesController < ApplicationController end def deactivate_confirm - @affected_logs = @scheme.lettings_logs.visible.after_date(params[:deactivation_date]) - if @affected_logs.count.zero? + @deactivation_date = Time.zone.parse(params[:deactivation_date]) + @affected_logs = @scheme.lettings_logs.visible.after_date(@deactivation_date) + @deactivation_date_type = params[:deactivation_date_type] + + scheme_locations = @scheme.locations.confirmed + + @affected_locations = scheme_locations.select do |location| + %i[active deactivating_soon reactivating_soon activating_soon].include?(location.status_at(@deactivation_date)) + end + + if @affected_logs.count.zero? && @affected_locations.count.zero? deactivate - else - @deactivation_date = params[:deactivation_date] - @deactivation_date_type = params[:deactivation_date_type] end end def deactivate - if @scheme.open_deactivation&.update!(deactivation_date: params[:deactivation_date]) || @scheme.scheme_deactivation_periods.create!(deactivation_date: params[:deactivation_date]) + deactivation_date = params[:deactivation_date] + if @scheme.open_deactivation&.update!(deactivation_date:) || @scheme.scheme_deactivation_periods.create!(deactivation_date:) logs = reset_location_and_scheme_for_logs! flash[:notice] = deactivate_success_notice diff --git a/app/helpers/deactivate_confirm_helper.rb b/app/helpers/deactivate_confirm_helper.rb new file mode 100644 index 000000000..a7e9edb19 --- /dev/null +++ b/app/helpers/deactivate_confirm_helper.rb @@ -0,0 +1,8 @@ +module DeactivateConfirmHelper + def affected_title(affected_logs, affected_locations) + title_parts = [] + title_parts << pluralize(affected_logs.count, "log") if affected_logs.count.positive? + title_parts << pluralize(affected_locations.count, "location") if affected_locations.count.positive? + "This change will affect #{title_parts.join(' and ')}." + end +end diff --git a/app/helpers/locations_helper.rb b/app/helpers/locations_helper.rb index f963c7040..fc1008926 100644 --- a/app/helpers/locations_helper.rb +++ b/app/helpers/locations_helper.rb @@ -70,7 +70,7 @@ module LocationsHelper def toggle_location_link(location) return govuk_button_link_to "Deactivate this location", scheme_location_new_deactivation_path(location.scheme, location), warning: true if location.active? || location.deactivates_in_a_long_time? - return govuk_button_link_to "Reactivate this location", scheme_location_new_reactivation_path(location.scheme, location) if location.deactivated? + return govuk_button_link_to "Reactivate this location", scheme_location_new_reactivation_path(location.scheme, location) if location.deactivated? && !location.deactivated_by_scheme? end def delete_location_link(location) @@ -100,14 +100,37 @@ private ActivePeriod = Struct.new(:from, :to) def location_active_periods(location) periods = [ActivePeriod.new(location.available_from, nil)] + location_deactivation_periods = location_deactivation_periods(location) + scheme_deactivation_periods = scheme_deactivation_periods(location, location_deactivation_periods) - sorted_deactivation_periods = remove_nested_periods(location.location_deactivation_periods.sort_by(&:deactivation_date)) + combined_deactivation_periods = location_deactivation_periods + scheme_deactivation_periods + sorted_deactivation_periods = combined_deactivation_periods.sort_by(&:deactivation_date) + + update_periods_with_deactivations(periods, sorted_deactivation_periods) + remove_overlapping_and_empty_periods(periods) + end + + def location_deactivation_periods(location) + periods = remove_nested_periods(location.location_deactivation_periods.sort_by(&:deactivation_date)) + periods.last&.deactivation_date if periods.last&.reactivation_date.nil? + periods + end + + def scheme_deactivation_periods(location, location_deactivation_periods) + return [] unless location.scheme.scheme_deactivation_periods.any? + + location_deactivation_date = location_deactivation_periods.last&.deactivation_date + periods = remove_nested_periods(location.scheme.scheme_deactivation_periods.sort_by(&:deactivation_date)) + periods.select do |period| + period.deactivation_date >= location.available_from && (location_deactivation_date.nil? || period.deactivation_date <= location_deactivation_date) + end + end + + def update_periods_with_deactivations(periods, sorted_deactivation_periods) sorted_deactivation_periods.each do |deactivation| periods.last.to = deactivation.deactivation_date periods << ActivePeriod.new(deactivation.reactivation_date, nil) end - - remove_overlapping_and_empty_periods(periods) end def remove_overlapping_and_empty_periods(periods) diff --git a/app/helpers/schemes_helper.rb b/app/helpers/schemes_helper.rb index f96f9e4c8..0e318d283 100644 --- a/app/helpers/schemes_helper.rb +++ b/app/helpers/schemes_helper.rb @@ -12,7 +12,7 @@ module SchemesHelper def toggle_scheme_link(scheme) return govuk_button_link_to "Deactivate this scheme", scheme_new_deactivation_path(scheme), warning: true if scheme.active? || scheme.deactivates_in_a_long_time? - return govuk_button_link_to "Reactivate this scheme", scheme_new_reactivation_path(scheme) if scheme.deactivated? + return govuk_button_link_to "Reactivate this scheme", scheme_new_reactivation_path(scheme) if scheme.deactivated? || scheme.deactivating_soon? end def delete_scheme_link(scheme) @@ -76,6 +76,15 @@ module SchemesHelper end end + def scheme_status_hint(scheme) + case scheme.status + when :deactivating_soon + "This scheme deactivates on #{scheme.last_deactivation_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Reactivate the scheme to add locations active after this date." + when :deactivated + "This scheme deactivated on #{scheme.last_deactivation_date.to_formatted_s(:govuk_date)}. Any locations you add will be deactivated on the same date. Reactivate the scheme to add locations active after this date." + end + end + private ActivePeriod = Struct.new(:from, :to) diff --git a/app/models/location.rb b/app/models/location.rb index 8efa4ee28..19cf5e211 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -40,6 +40,7 @@ class Location < ApplicationRecord filtered_records = filtered_records .left_outer_joins(:location_deactivation_periods) .joins(scheme: [:owning_organisation]) + .left_outer_joins(scheme: :scheme_deactivation_periods) .order("location_deactivation_periods.created_at DESC") .merge(scopes.reduce(&:or)) end @@ -52,30 +53,47 @@ class Location < ApplicationRecord .or(where(confirmed: nil)) } - scope :deactivated, lambda { + scope :deactivated, lambda { |date = Time.zone.now| deactivated_by_organisation - .or(deactivated_directly) + .or(deactivated_directly(date)) + .or(deactivated_by_scheme(date)) } scope :deactivated_by_organisation, lambda { merge(Organisation.filter_by_inactive) } + scope :deactivated_by_scheme, lambda { |date = Time.zone.now| + merge(Scheme.deactivated_directly(date)) + } + scope :deactivated_directly, lambda { |date = Time.zone.now| merge(LocationDeactivationPeriod.deactivations_without_reactivation) .where("location_deactivation_periods.deactivation_date <= ?", date) } - scope :deactivating_soon, lambda { |date = Time.zone.now| + scope :deactivating_soon_directly, lambda { |date = Time.zone.now| merge(LocationDeactivationPeriod.deactivations_without_reactivation) - .where("location_deactivation_periods.deactivation_date > ?", date) - .where.not(id: joins(scheme: [:owning_organisation]).deactivated_by_organisation.pluck(:id)) + .where("location_deactivation_periods.deactivation_date > ?", date) + } + + scope :deactivating_soon, lambda { |date = Time.zone.now| + deactivating_soon_directly + .or(deactivating_soon_by_scheme(date)) + } + + scope :deactivating_soon_by_scheme, lambda { |date = Time.zone.now| + merge(Scheme.deactivating_soon(date)) } scope :reactivating_soon, lambda { |date = Time.zone.now| where.not("location_deactivation_periods.reactivation_date IS NULL") - .where("location_deactivation_periods.reactivation_date > ?", date) - .where.not(id: joins(scheme: [:owning_organisation]).deactivated_by_organisation.pluck(:id)) + .where("location_deactivation_periods.reactivation_date > ?", date) + .where.not(id: joins(scheme: [:owning_organisation]).deactivated_by_organisation.pluck(:id)) + } + + scope :reactivating_soon_by_scheme, lambda { |date = Time.zone.now| + merge(Scheme.reactivating_soon(date)) } scope :activating_soon, lambda { |date = Time.zone.now| @@ -84,19 +102,21 @@ class Location < ApplicationRecord scope :active_status, lambda { where.not(id: joins(:location_deactivation_periods).reactivating_soon.pluck(:id)) - .where.not(id: joins(scheme: [:owning_organisation]).deactivated_by_organisation.pluck(:id)) - .where.not(id: joins(:location_deactivation_periods).deactivated_directly.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)) + .where.not(id: joins(scheme: [:scheme_deactivation_periods]).reactivating_soon_by_scheme.pluck(:id)) + .where.not(id: joins(:location_deactivation_periods).merge(Location.deactivated_directly).pluck(:id)) + .where.not(id: joins(scheme: [:scheme_deactivation_periods]).merge(Location.deactivated_by_scheme).pluck(:id)) + .where.not(id: joins(scheme: [:owning_organisation]).merge(Location.deactivated_by_organisation).pluck(:id)) + .where.not(id: incomplete.pluck(:id)) + .where.not(id: joins(:location_deactivation_periods).merge(Location.deactivating_soon_directly).pluck(:id)) + .where.not(id: joins(scheme: %i[owning_organisation scheme_deactivation_periods]).merge(Location.deactivating_soon_by_scheme).pluck(:id)) + .where.not(id: activating_soon.pluck(:id)) } scope :active, lambda { |date = Time.zone.now| - where.not(id: joins(:location_deactivation_periods).reactivating_soon(date).pluck(:id)) - .where.not(id: joins(scheme: [:owning_organisation]).deactivated_by_organisation.pluck(:id)) - .where.not(id: joins(:location_deactivation_periods).deactivated_directly(date).pluck(:id)) - .where.not(id: incomplete.pluck(:id)) + where.not(id: joins(:location_deactivation_periods).merge(Location.deactivated_directly(date)).pluck(:id)) + .where.not(id: incomplete.pluck(:id)) .where.not(id: activating_soon(date).pluck(:id)) + .where(scheme: Scheme.active(date)) } scope :visible, -> { where(discarded_at: nil) } @@ -163,10 +183,10 @@ class Location < ApplicationRecord return :deleted if discarded_at.present? return :incomplete unless confirmed return :deactivated if scheme.owning_organisation.status_at(date) == :deactivated || - open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date + open_deactivation&.deactivation_date.present? && date >= open_deactivation.deactivation_date || scheme.status_at(date) == :deactivated + return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date || scheme.status_at(date) == :deactivating_soon return :activating_soon if startdate.present? && date < startdate - return :deactivating_soon if open_deactivation&.deactivation_date.present? && date < open_deactivation.deactivation_date - return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date + return :reactivating_soon if last_deactivation_before(date)&.reactivation_date.present? && date < last_deactivation_before(date).reactivation_date || scheme.status_at(date) == :reactivating_soon :active end @@ -187,6 +207,18 @@ class Location < ApplicationRecord status_at(6.months.from_now) == :deactivating_soon end + def deactivated_by_scheme? + status == :deactivated && scheme.status == :deactivated + end + + def deactivating_soon_by_scheme? + status == :deactivating_soon && scheme.status == :deactivating_soon + end + + def reactivating_soon_by_scheme? + status == :reactivating_soon && scheme.status == :reactivating_soon + end + def validate_postcode if !postcode&.match(POSTCODE_REGEXP) error_message = I18n.t("validations.postcode") diff --git a/app/models/scheme.rb b/app/models/scheme.rb index 07ec14731..6d3524723 100644 --- a/app/models/scheme.rb +++ b/app/models/scheme.rb @@ -61,25 +61,27 @@ class Scheme < ApplicationRecord merge(Organisation.filter_by_inactive) } - scope :deactivated_directly, lambda { + scope :deactivated_directly, lambda { |date = Time.zone.now| merge(SchemeDeactivationPeriod.deactivations_without_reactivation) - .where("scheme_deactivation_periods.deactivation_date <= ?", Time.zone.now) + .where("scheme_deactivation_periods.deactivation_date <= ?", date) } - scope :deactivating_soon, lambda { + scope :deactivating_soon, lambda { |date = Time.zone.now| merge(SchemeDeactivationPeriod.deactivations_without_reactivation) - .where("scheme_deactivation_periods.deactivation_date > ? AND scheme_deactivation_periods.deactivation_date < ? ", Time.zone.now, 6.months.from_now) + .where("scheme_deactivation_periods.deactivation_date > ? AND scheme_deactivation_periods.deactivation_date < ? ", date, 6.months.from_now) .where.not(id: joins(:owning_organisation).deactivated_by_organisation.pluck(:id)) } - scope :reactivating_soon, lambda { - where.not("scheme_deactivation_periods.reactivation_date IS NULL") - .where("scheme_deactivation_periods.reactivation_date > ?", Time.zone.now) - .where.not(id: joins(:owning_organisation).deactivated_by_organisation.pluck(:id)) + scope :reactivating_soon, lambda { |date = Time.zone.now| + merge(SchemeDeactivationPeriod.deactivations_with_reactivation) + .where.not("scheme_deactivation_periods.reactivation_date IS NULL") + .where("scheme_deactivation_periods.reactivation_date > ?", date) + .where("scheme_deactivation_periods.deactivation_date <= ?", date) + .where.not(id: joins(:owning_organisation).deactivated_by_organisation.pluck(:id)) } - scope :activating_soon, lambda { - where("schemes.startdate > ?", Time.zone.now) + scope :activating_soon, lambda { |date = Time.zone.now| + where("schemes.startdate > ?", date) } scope :active_status, lambda { @@ -91,6 +93,14 @@ class Scheme < ApplicationRecord .where.not(id: activating_soon.pluck(:id)) } + scope :active, lambda { |date = Time.zone.now| + where.not(id: joins(:scheme_deactivation_periods).reactivating_soon(date).pluck(:id)) + .where.not(id: incomplete.pluck(:id)) + .where.not(id: joins(:owning_organisation).deactivated_by_organisation.pluck(:id)) + .where.not(id: joins(:owning_organisation).joins(:scheme_deactivation_periods).deactivated_directly(date).pluck(:id)) + .where.not(id: activating_soon(date).pluck(:id)) + } + scope :visible, -> { where(discarded_at: nil) } validate :validate_confirmed @@ -275,6 +285,10 @@ class Scheme < ApplicationRecord scheme_deactivation_periods.where("deactivation_date <= ?", date).order("created_at").last end + def last_deactivation_date + scheme_deactivation_periods.order(deactivation_date: :desc).first&.deactivation_date + end + def status @status ||= status_at(Time.zone.now) end @@ -313,6 +327,10 @@ class Scheme < ApplicationRecord status == :deactivated end + def deactivating_soon? + status == :deactivating_soon + end + def deactivates_in_a_long_time? status_at(6.months.from_now) == :deactivating_soon end diff --git a/app/models/scheme_deactivation_period.rb b/app/models/scheme_deactivation_period.rb index 176d15211..cb27534f7 100644 --- a/app/models/scheme_deactivation_period.rb +++ b/app/models/scheme_deactivation_period.rb @@ -48,4 +48,5 @@ class SchemeDeactivationPeriod < ApplicationRecord attr_accessor :deactivation_date_type, :reactivation_date_type scope :deactivations_without_reactivation, -> { where(reactivation_date: nil) } + scope :deactivations_with_reactivation, -> { where.not(reactivation_date: nil) } end diff --git a/app/models/validations/shared_validations.rb b/app/models/validations/shared_validations.rb index ab9f9d7a8..217b2c170 100644 --- a/app/models/validations/shared_validations.rb +++ b/app/models/validations/shared_validations.rb @@ -102,7 +102,11 @@ module Validations::SharedValidations return unless %i[reactivating_soon activating_soon deactivated].include?(status) closest_reactivation = resource.last_deactivation_before(date) - open_deactivation = resource.open_deactivation + open_deactivation = if resource.is_a?(Location) + resource.open_deactivation || resource.scheme.open_deactivation + else + resource.open_deactivation + end date = case status when :reactivating_soon then closest_reactivation.reactivation_date diff --git a/app/views/locations/index.html.erb b/app/views/locations/index.html.erb index 78a362332..64d9bf286 100644 --- a/app/views/locations/index.html.erb +++ b/app/views/locations/index.html.erb @@ -56,10 +56,17 @@ <% end %> <% end %> + <% if status_hint_message = scheme_status_hint(@scheme) %> +