From 372bb6238f1285c5a0fabcfb39b6f6f2e1b71427 Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Mon, 10 Jun 2024 14:55:24 -0400 Subject: [PATCH 01/11] Scaffold for parked client details --- .rubocop_todo.yml | 4 - app/controllers/clients_controller.rb | 4 +- ...tch_decision_acknowledgments_controller.rb | 27 ++-- app/controllers/match_decisions_controller.rb | 13 +- app/controllers/matches_controller.rb | 2 +- .../opportunity_matches_controller.rb | 6 +- .../qualified_opportunities_controller.rb | 2 +- .../reports/parked_clients_controller.rb | 29 +++- app/models/client.rb | 81 +---------- app/models/client_opportunity_match.rb | 34 +++-- app/models/concerns/availability.rb | 111 +++++++++++--- app/models/data_source.rb | 2 - app/models/match_decisions/base.rb | 6 +- .../confirm_match_success_dnd_staff.rb | 9 +- .../eight/eight_confirm_match_success.rb | 2 +- .../match_decisions/five/five_lease_up.rb | 2 +- .../four/confirm_match_success_dnd_staff.rb | 2 +- ...rm_hsa_accepts_client_decline_dnd_staff.rb | 2 +- ...r_decline_housing_subsidy_administrator.rb | 2 +- .../nine/nine_confirm_match_success.rb | 2 +- ...rm_hsa_accepts_client_decline_dnd_staff.rb | 2 +- .../provider_only/hsa_accepts_client.rb | 2 +- .../seven/confirm_match_success_dnd_staff.rb | 2 +- .../six/confirm_match_success_dnd_staff.rb | 2 +- .../ten_confirm_match_success_dnd_staff.rb | 2 +- app/models/opportunity.rb | 4 +- app/models/unavailable_as_candidate_for.rb | 22 ++- app/views/reports/parked_clients/index.haml | 8 +- config/environments/test.rb | 2 + db/schema.rb | 7 +- lib/tasks/cas.rake | 1 - spec/models/client_opportunity_match_spec.rb | 136 +++++++++--------- spec/models/decisions_spec.rb | 2 +- 33 files changed, 287 insertions(+), 247 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4d92daf02..a4e46e2c6 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -343,7 +343,6 @@ Layout/SpaceAroundOperators: # SupportedStylesForEmptyBraces: space, no_space Layout/SpaceBeforeBlockBraces: Exclude: - - 'spec/models/client_opportunity_match_spec.rb' - 'spec/models/imported_clients_csv_spec.rb' - 'spec/tasks/update_project_clients_from_deidentified_clients_spec.rb' @@ -724,7 +723,6 @@ Style/BlockComments: Style/BlockDelimiters: Exclude: - 'spec/controllers/client_notes_controller_spec.rb' - - 'spec/models/client_opportunity_match_spec.rb' - 'spec/models/prioritization/rank_spec.rb' - 'spec/models/rules/assessment_scores_spec.rb' - 'spec/models/rules/assessment_scores_specified_spec.rb' @@ -813,7 +811,6 @@ Style/InverseMethods: # Configuration parameters: IgnoredMethods. Style/MethodCallWithoutArgsParentheses: Exclude: - - 'spec/models/decisions_spec.rb' # Offense count: 2 # Cop supports --auto-correct. @@ -999,7 +996,6 @@ Style/StringLiterals: - 'spec/factories/housing_media_links.rb' - 'spec/factories/roles.rb' - 'spec/models/client_notes_spec.rb' - - 'spec/models/client_opportunity_match_spec.rb' - 'spec/models/prioritization/rank_spec.rb' - 'spec/requests/express_neighborhood_interests_spec.rb' - 'spec/support/tasks.rb' diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb index 288c04d7f..0ebac7f65 100644 --- a/app/controllers/clients_controller.rb +++ b/app/controllers/clients_controller.rb @@ -86,7 +86,7 @@ def update prevent_matching_until = params[:client].try(:[], :prevent_matching_until) should_prevent_matching = prevent_matching_until.present? && prevent_matching_until.to_date > Date.current - @client.unavailable(permanent: false, contact_id: current_contact.id, cancel_all: true, expires_at: prevent_matching_until.to_date) if should_prevent_matching + @client.unavailable(permanent: false, contact_id: current_contact.id, cancel_all: true, expires_at: prevent_matching_until.to_date, user: current_user) if should_prevent_matching if request.xhr? head :ok @@ -105,7 +105,7 @@ def update # Remove the client from any other proposed matches # Mark the Client as available = false def unavailable - @client.unavailable(permanent: true) + @client.unavailable(permanent: true, user: current_user) redirect_to action: :show end diff --git a/app/controllers/match_decision_acknowledgments_controller.rb b/app/controllers/match_decision_acknowledgments_controller.rb index b2827c853..6b06fc17c 100644 --- a/app/controllers/match_decision_acknowledgments_controller.rb +++ b/app/controllers/match_decision_acknowledgments_controller.rb @@ -13,32 +13,27 @@ class MatchDecisionAcknowledgmentsController < ApplicationController before_action :find_decision! before_action :authorize_decision! - delegate :current_contact, - :match_scope, - to: :access_context + delegate :current_contact, :match_scope, to: :access_context def create if @decision.update status: 'acknowledged' @decision.record_action_event! contact: current_contact - @decision.run_status_callback! + @decision.run_status_callback!(user: current_user) head :ok else head :bad_request end end - private - - def find_match! - @match = match_scope.find params[:match_id] - end - - def find_decision! - @decision = @match.decision_from_param params[:decision_id] - end + private def find_match! + @match = match_scope.find params[:match_id] + end - def authorize_decision! - # TODO ensure that the current contact can authorize this decision - end + private def find_decision! + @decision = @match.decision_from_param params[:decision_id] + end + private def authorize_decision! + # TODO ensure that the current contact can authorize this decision + end end diff --git a/app/controllers/match_decisions_controller.rb b/app/controllers/match_decisions_controller.rb index 3540c5713..d7b9ed196 100644 --- a/app/controllers/match_decisions_controller.rb +++ b/app/controllers/match_decisions_controller.rb @@ -36,7 +36,7 @@ def show render 'matches/show' end - def update + def update # rubocop:disable Metrics/AbcSize @program = @match.program @sub_program = @match.sub_program @types = MatchRoutes::Base.match_steps @@ -114,9 +114,16 @@ def update # re-enable the client for matching prevent_matching_until = decision_params[:prevent_matching_until]&.to_date if can_reject_matches? && prevent_matching_until.present? && prevent_matching_until > Date.current - @match.client.unavailable(permanent: false, contact_id: current_contact.id, cancel_specific: @match, expires_at: prevent_matching_until) + @match.client.unavailable( + permanent: false, + contact_id: current_contact.id, + cancel_specific: @match, + expires_at: prevent_matching_until, + user: current_user, + match: @match, + ) else - @decision.run_status_callback! + @decision.run_status_callback!(user: current_user) end if decision_params[:disable_opportunity] == '1' diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb index 0875179c7..daf69031b 100644 --- a/app/controllers/matches_controller.rb +++ b/app/controllers/matches_controller.rb @@ -30,7 +30,7 @@ def history def reopen @match = match_scope.find(params[:match_id]) - @match.reopen!(current_contact) + @match.reopen!(current_contact, user: current_user) redirect_to match_path(@match) end diff --git a/app/controllers/opportunity_matches_controller.rb b/app/controllers/opportunity_matches_controller.rb index d000e8e12..464d58d5f 100644 --- a/app/controllers/opportunity_matches_controller.rb +++ b/app/controllers/opportunity_matches_controller.rb @@ -36,13 +36,13 @@ def create client_ids_to_activate.each do |client_id| match = ClientOpportunityMatch.where(client_id: client_id, opportunity_id: @opportunity, closed: false). first_or_create(create_match_attributes(client_id)) - match.activate!(touch_referral_event: @opportunity.match_route.auto_initialize_event?) + match.activate!(touch_referral_event: @opportunity.match_route.auto_initialize_event?, user: current_user) end redirect_to opportunity_matches_path(@opportunity) end def update - client_id = params[:id].to_i + client_id = params[:id].to_i unless @opportunity.match_route.allow_multiple_active_matches @opportunity.active_matches.each do |active_match| @@ -53,7 +53,7 @@ def update match = ClientOpportunityMatch.create(create_match_attributes(client_id)) - match.activate!(touch_referral_event: @opportunity.match_route.auto_initialize_event?) + match.activate!(touch_referral_event: @opportunity.match_route.auto_initialize_event?, user: current_user) redirect_to match_path match end diff --git a/app/controllers/qualified_opportunities_controller.rb b/app/controllers/qualified_opportunities_controller.rb index e3b21c6cf..d892cd826 100644 --- a/app/controllers/qualified_opportunities_controller.rb +++ b/app/controllers/qualified_opportunities_controller.rb @@ -40,7 +40,7 @@ def update match_route: @opportunity.match_route, universe_state: universe_state, ) - match.activate!(touch_referral_event: @opportunity.match_route.auto_initialize_event?) + match.activate!(touch_referral_event: @opportunity.match_route.auto_initialize_event?, user: current_user) redirect_to match_path match end diff --git a/app/controllers/reports/parked_clients_controller.rb b/app/controllers/reports/parked_clients_controller.rb index d4061ca05..85df1a429 100644 --- a/app/controllers/reports/parked_clients_controller.rb +++ b/app/controllers/reports/parked_clients_controller.rb @@ -14,15 +14,30 @@ def index @active_tab = params[:tab] || route_to_html_id(@routes.first) clients = parked_scope clients = clients.non_confidential.full_release unless can_view_all_clients? - - @clients = clients. - joins(:unavailable_as_candidate_fors). - order(uacf_t[:expires_at].asc). - page(params[:page].to_i).per(25) + respond_to do |format| + format.html do + @clients = clients. + joins(:unavailable_as_candidate_fors). + order(uacf_t[:expires_at].asc). + page(params[:page].to_i).per(25) + end + format.xlsx do + @parked = UnavailableAsCandidateFor.joins(:client). + merge(client_scope). + where(match_route_type: MatchRoutes::Base.active.select(:type)). + preload(:user, client: { project_client: :data_source }) + filename = "Parked Clients #{Time.current.to_fs(:db)}" + render xlsx: 'index', filename: filename + end + end end def parked_scope - Client.accessible_by_user(current_user).unavailable_in(@route) + client_scope.unavailable_in(@route) + end + + private def client_scope + Client.accessible_by_user(current_user) end private def set_routes @@ -38,7 +53,7 @@ def route_to_html_id(route) end helper_method :route_to_html_id - private def route_from_tab(tab_label=nil) + private def route_from_tab(tab_label = nil) return @routes.first unless tab_label.present? @routes.detect do |route| diff --git a/app/models/client.rb b/app/models/client.rb index b7e80a8d6..a30d6dde7 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -368,85 +368,6 @@ def active_in_match? active_matches.any? end - def available_as_candidate_for_any_route? - ( - MatchRoutes::Base.available.pluck(:type) - - UnavailableAsCandidateFor.where(client_id: id).distinct.pluck(:match_route_type) - ).any? - end - - def make_available_in match_route: - route_name = MatchRoutes::Base.route_name_from_route(match_route) - UnavailableAsCandidateFor.where(client_id: id, match_route_type: route_name).destroy_all - end - - def make_available_in_all_routes - UnavailableAsCandidateFor.where(client_id: id).destroy_all - end - - private def default_unavailable_expiration_date - days = Config.get(:unavailable_for_length) - return nil unless days.present? && days.positive? - - Date.current + days.days - end - - def make_unavailable_in(match_route:, expires_at: default_unavailable_expiration_date) - route_name = MatchRoutes::Base.route_name_from_route(match_route) - - un = unavailable_as_candidate_fors.where(match_route_type: route_name).first_or_create - un.update(expires_at: expires_at) - end - - def make_unavailable_in_all_routes(expires_at: default_unavailable_expiration_date) - MatchRoutes::Base.all_routes.each do |route| - make_unavailable_in(match_route: route, expires_at: expires_at) - end - end - - # cancel_specific must be a match object - def unavailable(permanent: false, contact_id: nil, cancel_all: false, cancel_specific: false, expires_at: default_unavailable_expiration_date) - Client.transaction do - update(available: false) if permanent - - if cancel_all - # Cancel any active matches - client_opportunity_matches.active.each do |active_match| - opportunity = active_match.opportunity - active_match.canceled! - MatchEvents::Parked.create!( - match_id: active_match.id, - contact_id: contact_id, - ) - opportunity.update(available_candidate: true) - end - # Delete any non-active open matches - client_opportunity_matches.open.each(&:delete) - # Prevent any new matches for this client - # This will re-queue the client once the date is passed - make_unavailable_in_all_routes(expires_at: expires_at) - end - - if cancel_specific - # remove from specific match and proposed on same route - match = cancel_specific - opportunity = match.opportunity - route = match.match_route - match.canceled! # note canceled! makes the client available in the route - Notifications::MatchCanceled.create_for_match! match - MatchEvents::Parked.create!( - match_id: match.id, - contact_id: contact_id, - ) - opportunity.update(available_candidate: true) - # Delete any non-active open matches - client_opportunity_matches.on_route(route).proposed.each(&:delete) - make_unavailable_in(match_route: route, expires_at: expires_at) - end - end - Matching::RunEngineJob.perform_later - end - def remote_id @remote_id ||= project_client&.id_in_data_source.presence end @@ -456,7 +377,7 @@ def remote_data_source_id end def remote_data_source - @remote_data_source ||= DataSource.find(remote_data_source_id) rescue false # rubocop:disable Style/RescueModifier + @remote_data_source ||= project_client&.data_source || false end def remote_client_visible_to?(user) diff --git a/app/models/client_opportunity_match.rb b/app/models/client_opportunity_match.rb index f444025fa..15c932de7 100644 --- a/app/models/client_opportunity_match.rb +++ b/app/models/client_opportunity_match.rb @@ -644,9 +644,9 @@ def restrictions_on_reopening restrictions end - def reopen!(contact) + def reopen!(contact, user: nil) self.class.transaction do - client.make_unavailable_in(match_route: match_route, expires_at: nil) + client.make_unavailable_in(match_route: match_route, expires_at: nil, user: user, match: self, reason: 'Active Match') update(closed: false, active: true, closed_reason: nil) current_decision.update(status: :pending) MatchEvents::Reopened.create(match_id: id, contact_id: contact.id) @@ -658,11 +658,11 @@ def reopen!(contact) end end - def activate!(touch_referral_event: true) + def activate!(touch_referral_event: true, user: nil) self.class.transaction do - update! active: true - client.make_unavailable_in(match_route: match_route, expires_at: nil) if match_route.should_prevent_multiple_matches_per_client - opportunity.update available_candidate: false + update!(active: true) + client.make_unavailable_in(match_route: match_route, expires_at: nil, user: user, match: self, reason: 'Active Match') if match_route.should_prevent_multiple_matches_per_client + opportunity.update(available_candidate: false) add_default_contacts! requirements_with_inherited.each { |req| req.apply_to_match(self) } send(match_route.initial_decision).initialize_decision! @@ -727,7 +727,7 @@ def poached!(touch_referral_event: true) end end - def succeeded!(touch_referral_event: true) + def succeeded!(touch_referral_event: true, user: nil) self.class.transaction do route = opportunity.match_route update! active: false, closed: true, closed_reason: 'success' @@ -736,9 +736,11 @@ def succeeded!(touch_referral_event: true) if route.should_cancel_other_matches client_related_matches.each do |match| if match.current_decision.present? - MatchEvents::DecisionAction.create(match_id: match.id, - decision_id: match.current_decision.id, - action: :canceled) + MatchEvents::DecisionAction.create( + match_id: match.id, + decision_id: match.current_decision.id, + action: :canceled, + ) reason = MatchDecisionReasons::AdministrativeCancel.find_by(name: 'Client received another housing opportunity') match.current_decision.update! status: 'canceled', administrative_cancel_reason_id: reason.id match.poached! @@ -748,10 +750,10 @@ def succeeded!(touch_referral_event: true) end client.update available: false # Prevent matching on any route - client.make_unavailable_in_all_routes + client.make_unavailable_in_all_routes(user: user, match: self, reason: 'Successful Match') else # Prevent matching on this route again - client.make_unavailable_in(match_route: route) + client.make_unavailable_in(match_route: route, user: user, match: self, reason: 'Successful Match') end # Cleanup other matches on the same opportunity @@ -761,9 +763,11 @@ def succeeded!(touch_referral_event: true) else opportunity_related_matches.each do |match| if match.active - MatchEvents::DecisionAction.create(match_id: match.id, - decision_id: match.current_decision.id, - action: :canceled) + MatchEvents::DecisionAction.create( + match_id: match.id, + decision_id: match.current_decision.id, + action: :canceled, + ) opportunity.notify_contacts_opportunity_taken(match) reason = MatchDecisionReasons::AdministrativeCancel.find_by(name: 'Vacancy filled by other client') match.current_decision.update! status: 'canceled', administrative_cancel_reason_id: reason.id diff --git a/app/models/concerns/availability.rb b/app/models/concerns/availability.rb index 02686b33c..97010c091 100644 --- a/app/models/concerns/availability.rb +++ b/app/models/concerns/availability.rb @@ -6,24 +6,9 @@ # Tools for ensuring client availability module Availability - extend ActiveSupport::Concern - included do - - # TODO: Disabled in the nightly processing, remove? - def self.ensure_availability - self.available.each(&:ensure_availability) - end - - def ensure_availability - self.class.transaction do - unavailable_as_candidate_fors.destroy_all - unavailable_routes.each do |route| - self.make_unavailable_in match_route: route - end - end - end + included do # any routes with active or successful matches def unavailable_routes (active_routes + successful_routes).uniq @@ -36,5 +21,99 @@ def active_routes def successful_routes client_opportunity_matches.success.map(&:match_route).uniq end + + def available_as_candidate_for_any_route? + ( + MatchRoutes::Base.available.pluck(:type) - + UnavailableAsCandidateFor.where(client_id: id).distinct.pluck(:match_route_type) + ).any? + end + + def make_available_in(match_route:) + route_name = MatchRoutes::Base.route_name_from_route(match_route) + UnavailableAsCandidateFor.where(client_id: id, match_route_type: route_name).destroy_all + end + + def make_available_in_all_routes + UnavailableAsCandidateFor.where(client_id: id).destroy_all + end + + def make_unavailable_in(match_route:, user:, reason:, expires_at: default_unavailable_expiration_date, match: nil) + route_name = MatchRoutes::Base.route_name_from_route(match_route) + + # Delete any previous unavailable_as_candidate_fors so we can track the reason for this one + unavailable_as_candidate_fors.where(match_route_type: route_name).destroy_all + + unavailable_as_candidate_fors.where(match_route_type: route_name).create( + expires_at: expires_at, + user_id: user&.id, + reason: reason, + match: match, + ) + end + + def make_unavailable_in_all_routes(user:, reason:, expires_at: default_unavailable_expiration_date, match: nil) + MatchRoutes::Base.all_routes.each do |route| + make_unavailable_in(match_route: route, expires_at: expires_at, user: user, reason: reason, match: match) + end + end + + # cancel_specific must be a match object + def unavailable( # rubocop:disable Metrics/ParameterLists + permanent: false, + contact_id: nil, + cancel_all: false, + cancel_specific: false, + expires_at: default_unavailable_expiration_date, + user: nil, + match: nil + ) + Client.transaction do + update(available: false) if permanent + + if cancel_all + # Cancel any active matches + client_opportunity_matches.active.each do |active_match| + opportunity = active_match.opportunity + active_match.canceled! + MatchEvents::Parked.create!( + match_id: active_match.id, + contact_id: contact_id, + ) + opportunity.update(available_candidate: true) + end + # Delete any non-active open matches + client_opportunity_matches.open.each(&:delete) + # Prevent any new matches for this client + # This will re-queue the client once the date is passed + make_unavailable_in_all_routes(expires_at: expires_at, user: user, match: match, reason: 'Parked') + end + + if cancel_specific + # remove from specific match and proposed on same route + match = cancel_specific + opportunity = match.opportunity + route = match.match_route + match.canceled! # note canceled! makes the client available in the route + Notifications::MatchCanceled.create_for_match! match + MatchEvents::Parked.create!( + match_id: match.id, + contact_id: contact_id, + ) + opportunity.update(available_candidate: true) + # Delete any non-active open matches + client_opportunity_matches.on_route(route).proposed.each(&:delete) + make_unavailable_in(match_route: route, expires_at: expires_at, user: user, match: match, reason: 'Parked') + end + end + Matching::RunEngineJob.perform_later + end + + private def default_unavailable_expiration_date + days = Config.get(:unavailable_for_length) + return nil unless days.present? && days.positive? + + Date.current + days.days + end end end diff --git a/app/models/data_source.rb b/app/models/data_source.rb index 73e48315e..c6fd2f595 100644 --- a/app/models/data_source.rb +++ b/app/models/data_source.rb @@ -5,7 +5,6 @@ ### class DataSource < ApplicationRecord - belongs_to :building has_many :building_clients has_many :project_clients @@ -19,5 +18,4 @@ class DataSource < ApplicationRecord scope :hmis, -> do where.not(db_identifier: 'Deidentified') end - end diff --git a/app/models/match_decisions/base.rb b/app/models/match_decisions/base.rb index 98070b4fd..b2798a82a 100644 --- a/app/models/match_decisions/base.rb +++ b/app/models/match_decisions/base.rb @@ -256,15 +256,15 @@ def status_label statuses[status && status.to_sym] end - def run_status_callback! **dependencies + def run_status_callback!(**dependencies) match.clear_current_decision_cache! - status_callbacks.new(self, dependencies).public_send status + status_callbacks.new(self, dependencies).public_send(status) end # inherit in subclasses and add methods for each entry in #statuses class StatusCallbacks attr_reader :decision, :dependencies - def initialize(decision, _options) + def initialize(decision, dependencies) @decision = decision @dependencies = dependencies end diff --git a/app/models/match_decisions/confirm_match_success_dnd_staff.rb b/app/models/match_decisions/confirm_match_success_dnd_staff.rb index b14692718..975b99f06 100644 --- a/app/models/match_decisions/confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/confirm_match_success_dnd_staff.rb @@ -6,7 +6,6 @@ module MatchDecisions class ConfirmMatchSuccessDndStaff < Base - # validate :note_present_if_status_rejected def statuses @@ -68,9 +67,7 @@ def accessible_by? contact end private def note_present_if_status_rejected - if note.blank? && status == 'rejected' - errors.add :note, 'Please note why the match is declined.' - end + errors.add :note, 'Please note why the match is declined.' if note.blank? && status == 'rejected' end def show_client_match_attributes? @@ -83,7 +80,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmedDevelopmentOfficer.create_for_match! match - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def rejected @@ -91,7 +88,5 @@ def rejected end end private_constant :StatusCallbacks - end - end diff --git a/app/models/match_decisions/eight/eight_confirm_match_success.rb b/app/models/match_decisions/eight/eight_confirm_match_success.rb index 5d1bf37db..784351b7c 100644 --- a/app/models/match_decisions/eight/eight_confirm_match_success.rb +++ b/app/models/match_decisions/eight/eight_confirm_match_success.rb @@ -66,7 +66,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def rejected diff --git a/app/models/match_decisions/five/five_lease_up.rb b/app/models/match_decisions/five/five_lease_up.rb index 093a0e5d9..c9dbbf2c3 100644 --- a/app/models/match_decisions/five/five_lease_up.rb +++ b/app/models/match_decisions/five/five_lease_up.rb @@ -68,7 +68,7 @@ def expiration_update end def completed - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def canceled diff --git a/app/models/match_decisions/four/confirm_match_success_dnd_staff.rb b/app/models/match_decisions/four/confirm_match_success_dnd_staff.rb index 90531713f..1cca629bd 100644 --- a/app/models/match_decisions/four/confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/four/confirm_match_success_dnd_staff.rb @@ -85,7 +85,7 @@ def pending def confirmed Notifications::Four::MatchSuccessConfirmed.create_for_match! match - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def rejected diff --git a/app/models/match_decisions/homeless_set_aside/set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb b/app/models/match_decisions/homeless_set_aside/set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb index 505d25b8e..c6a9deb7c 100644 --- a/app/models/match_decisions/homeless_set_aside/set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb +++ b/app/models/match_decisions/homeless_set_aside/set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb @@ -73,7 +73,7 @@ def pending end def decline_overridden - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def decline_overridden_returned diff --git a/app/models/match_decisions/homeless_set_aside/set_asides_record_client_housed_date_or_decline_housing_subsidy_administrator.rb b/app/models/match_decisions/homeless_set_aside/set_asides_record_client_housed_date_or_decline_housing_subsidy_administrator.rb index 4d19f626e..b96eeef71 100644 --- a/app/models/match_decisions/homeless_set_aside/set_asides_record_client_housed_date_or_decline_housing_subsidy_administrator.rb +++ b/app/models/match_decisions/homeless_set_aside/set_asides_record_client_housed_date_or_decline_housing_subsidy_administrator.rb @@ -120,7 +120,7 @@ def other_clients_canceled end def completed - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def declined diff --git a/app/models/match_decisions/nine/nine_confirm_match_success.rb b/app/models/match_decisions/nine/nine_confirm_match_success.rb index 5838a3d48..3a7706e5f 100644 --- a/app/models/match_decisions/nine/nine_confirm_match_success.rb +++ b/app/models/match_decisions/nine/nine_confirm_match_success.rb @@ -65,7 +65,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def rejected diff --git a/app/models/match_decisions/provider_only/confirm_hsa_accepts_client_decline_dnd_staff.rb b/app/models/match_decisions/provider_only/confirm_hsa_accepts_client_decline_dnd_staff.rb index ef20aaa68..cfecbe867 100644 --- a/app/models/match_decisions/provider_only/confirm_hsa_accepts_client_decline_dnd_staff.rb +++ b/app/models/match_decisions/provider_only/confirm_hsa_accepts_client_decline_dnd_staff.rb @@ -79,7 +79,7 @@ def pending end def decline_overridden - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def decline_overridden_returned diff --git a/app/models/match_decisions/provider_only/hsa_accepts_client.rb b/app/models/match_decisions/provider_only/hsa_accepts_client.rb index 6fb423955..e03856c72 100644 --- a/app/models/match_decisions/provider_only/hsa_accepts_client.rb +++ b/app/models/match_decisions/provider_only/hsa_accepts_client.rb @@ -101,7 +101,7 @@ def pending def accepted Notifications::ProviderOnly::HsaAcceptsClientSspNotification.create_for_match! match - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def declined diff --git a/app/models/match_decisions/seven/confirm_match_success_dnd_staff.rb b/app/models/match_decisions/seven/confirm_match_success_dnd_staff.rb index 37389c881..5f7c71ca4 100644 --- a/app/models/match_decisions/seven/confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/seven/confirm_match_success_dnd_staff.rb @@ -79,7 +79,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def rejected diff --git a/app/models/match_decisions/six/confirm_match_success_dnd_staff.rb b/app/models/match_decisions/six/confirm_match_success_dnd_staff.rb index 9392881e6..6938184b6 100644 --- a/app/models/match_decisions/six/confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/six/confirm_match_success_dnd_staff.rb @@ -96,7 +96,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def rejected diff --git a/app/models/match_decisions/ten/ten_confirm_match_success_dnd_staff.rb b/app/models/match_decisions/ten/ten_confirm_match_success_dnd_staff.rb index 9ad7db149..4d5d3cb2a 100644 --- a/app/models/match_decisions/ten/ten_confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/ten/ten_confirm_match_success_dnd_staff.rb @@ -86,7 +86,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded! + match.succeeded!(user: @dependencies.try(:[], :user)) end def declined diff --git a/app/models/opportunity.rb b/app/models/opportunity.rb index 76a9fc1d2..7d36797e0 100644 --- a/app/models/opportunity.rb +++ b/app/models/opportunity.rb @@ -216,11 +216,11 @@ def confidential? # remove voucher # remove self (opportunity) - def stop_matches_and_remove_entire_history_from_existance! + def stop_matches_and_remove_entire_history_from_existence! Opportunity.transaction do client_opportunity_matches.each do |match| match.client.update(available: true) - match.client.make_unavailable_in(match_route: match_route) + match.client.make_unavailable_in(match_route: match_route, match: match) match.client_opportunity_match_contacts.destroy_all match.notifications.destroy_all match.destroy diff --git a/app/models/unavailable_as_candidate_for.rb b/app/models/unavailable_as_candidate_for.rb index 05c8ac47f..1068de944 100644 --- a/app/models/unavailable_as_candidate_for.rb +++ b/app/models/unavailable_as_candidate_for.rb @@ -8,11 +8,14 @@ # specific match route, existence of a record in this table for a given route # means the client is unavailable. class UnavailableAsCandidateFor < ApplicationRecord + include ArelHelper belongs_to :client belongs_to :route, class_name: 'MatchRoutes::Base', primary_key: :type, foreign_key: :match_route_type + belongs_to :user, optional: true + belongs_to :match, class_name: 'ClientOpportunityMatch', optional: true has_paper_trail - scope :for_route, -> (route) do + scope :for_route, ->(route) do route_name = MatchRoutes::Base.route_name_from_route(route) where(match_route_type: route_name) end @@ -40,7 +43,22 @@ def self.describe_expiration_length def self.cleanup_expired! where.not(expires_at: nil). - where(arel_table[:expires_at].lt(Date.current)).destroy_all + where(arel_table[:expires_at].lt(Date.current)). + destroy_all end + def self.download_columns + { + 'First Name' => ->(parked) { parked.client.first_name }, + 'Last Name' => ->(parked) { parked.client.last_name }, + 'CAS Client ID' => ->(parked) { parked.client_id }, + 'Remote ID' => ->(parked) { parked.client.remote_id }, + 'Remote Source' => ->(parked) { parked.client.remote_data_source&.name }, + 'Route' => ->(parked) { parked.route.title }, + 'Parked On' => ->(parked) { parked.created_at.to_date }, + 'Parked Until' => ->(parked) { parked.expires_at&.to_date || 'Indefinite' }, + # 'Parked By' => :user_id, + # 'Parked Reason' => :reason, + } + end end diff --git a/app/views/reports/parked_clients/index.haml b/app/views/reports/parked_clients/index.haml index 109857fc0..9e60c0e24 100644 --- a/app/views/reports/parked_clients/index.haml +++ b/app/views/reports/parked_clients/index.haml @@ -14,7 +14,13 @@ %li.nav-item{role: :presentation} = link_to route_name, reports_parked_clients_path(tab: tab_label), class: "nav-link #{active}" - if @clients.any? - %p= "The following clients have been removed temporarily from matching the #{@route.title}" + .d-flex + %p= "The following clients have been removed temporarily from matching the #{@route.title}" + .ml-auto + = link_to reports_parked_clients_path(format: :xlsx), class: 'btn btn-secondary' do + %i.icon-download2 + Download + %p= page_entries_info @clients .table-responsive .c-card.c.card__flush.mb-4 diff --git a/config/environments/test.rb b/config/environments/test.rb index db22db643..3c6bd34bb 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -39,6 +39,8 @@ config.action_mailer.perform_caching = false + config.active_job.queue_adapter = :test + # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. diff --git a/db/schema.rb b/db/schema.rb index 05047ff0f..b8777f6f1 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: 2024_05_08_140355) do +ActiveRecord::Schema[7.0].define(version: 2024_06_10_132826) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1750,8 +1750,13 @@ t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.datetime "expires_at", precision: nil + t.bigint "user_id" + t.bigint "match_id" + t.string "reason" t.index ["client_id"], name: "index_unavailable_as_candidate_fors_on_client_id" + t.index ["match_id"], name: "index_unavailable_as_candidate_fors_on_match_id" t.index ["match_route_type"], name: "index_unavailable_as_candidate_fors_on_match_route_type" + t.index ["user_id"], name: "index_unavailable_as_candidate_fors_on_user_id" end create_table "units", id: :serial, force: :cascade do |t| diff --git a/lib/tasks/cas.rake b/lib/tasks/cas.rake index 6d3df98e0..c832c61be 100644 --- a/lib/tasks/cas.rake +++ b/lib/tasks/cas.rake @@ -7,7 +7,6 @@ namespace :cas do Warehouse::BuildReport.new.run! if Warehouse::Base.enabled? Warehouse::FlagHoused.new.run! if Warehouse::Base.enabled? - # Client.ensure_availability Cas::UpdateVoucherAvailability.new.run! UnavailableAsCandidateFor.cleanup_expired! Matching::RunEngineJob.perform_later diff --git a/spec/models/client_opportunity_match_spec.rb b/spec/models/client_opportunity_match_spec.rb index 67c2ee668..1919805e5 100644 --- a/spec/models/client_opportunity_match_spec.rb +++ b/spec/models/client_opportunity_match_spec.rb @@ -6,98 +6,98 @@ let(:priority) { create :priority_vispdat_priority } let(:default_route) { create :default_route, match_prioritization: priority } let(:provider_route) { create :provider_route, match_prioritization: priority } - describe "Match initiation on the default route" do - describe "when the initial user has email schedule set to immediate" do + describe 'Match initiation on the default route' do + describe 'when the initial user has email schedule set to immediate' do let!(:user) { create :user, receive_initial_notification: true, email_schedule: :immediate } - let!(:program) { + let!(:program) do program = match.sub_program.program program.match_route = default_route program.save match.match_route = program.match_route match.save - } + end it 'a notification is created' do match.dnd_staff_contacts << user.contact - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { Notifications::Base.count }.by(1) end it 'a message is added' do match.dnd_staff_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { Message.count }.by(1) end end it 'a notification job is queued' do match.dnd_staff_contacts << user.contact - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { ActiveJob::Base.queue_adapter.enqueued_jobs.size }.by(1) end it 'it sends an email' do match.dnd_staff_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { ActionMailer::Base.deliveries.size }.by(1) end end end - describe "when the initial user has email schedule set to daily" do + describe 'when the initial user has email schedule set to daily' do let!(:user) { create :user, receive_initial_notification: true, email_schedule: :daily } - let!(:program) { + let!(:program) do program = match.sub_program.program program.match_route = default_route program.save match.match_route = program.match_route match.save - } + end it 'a notification is created' do match.dnd_staff_contacts << user.contact - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { Notifications::Base.count }.by(1) end it 'a message is added' do match.dnd_staff_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { Message.count }.by(1) end end it 'a notification job is queued' do match.dnd_staff_contacts << user.contact - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { ActiveJob::Base.queue_adapter.enqueued_jobs.size }.by(1) end it 'it does not send an email' do match.dnd_staff_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { ActionMailer::Base.deliveries.size }.by(0) end @@ -105,10 +105,10 @@ it 'running digest daily sends an email' do match.dnd_staff_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! + expect do + match.activate!(user: user) MessageJob.new('daily').perform - }.to change{ + end.to change { ActionMailer::Base.deliveries.size }.by(1) end @@ -116,88 +116,88 @@ end end - describe "Match initiation on the provider only route" do - describe "when the initial user has email schedule set to immediate" do + describe 'Match initiation on the provider only route' do + describe 'when the initial user has email schedule set to immediate' do let!(:user) { create :user, receive_initial_notification: true, email_schedule: :immediate } - let!(:program) { + let!(:program) do program = match.sub_program.program program.match_route = provider_route program.save match.match_route = program.match_route match.save - } + end it 'a notification is created' do match.housing_subsidy_admin_contacts << user.contact - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { Notifications::Base.count }.by(1) end it 'a message is added' do match.housing_subsidy_admin_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { Message.count }.by(1) end end it 'a notification job is queued' do match.housing_subsidy_admin_contacts << user.contact - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { ActiveJob::Base.queue_adapter.enqueued_jobs.size }.by(1) end it 'it sends an email' do match.housing_subsidy_admin_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { ActionMailer::Base.deliveries.size }.by(1) end end end - describe "when the initial user has email schedule set to daily" do + describe 'when the initial user has email schedule set to daily' do let!(:user) { create :user, receive_initial_notification: true, email_schedule: :daily } - let!(:program) { + let!(:program) do program = match.sub_program.program program.match_route = provider_route program.save match.match_route = program.match_route match.save - } + end it 'a notification is created' do match.housing_subsidy_admin_contacts << user.contact - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { Notifications::Base.count }.by(1) end it 'a notification job is queued' do match.housing_subsidy_admin_contacts << user.contact - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { ActiveJob::Base.queue_adapter.enqueued_jobs.size }.by(1) end it 'a message is added' do match.housing_subsidy_admin_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { Message.count }.by(1) end @@ -205,9 +205,9 @@ it 'it does not send an email' do match.housing_subsidy_admin_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! - }.to change{ + expect do + match.activate!(user: user) + end.to change { ActionMailer::Base.deliveries.size }.by(0) end @@ -215,10 +215,10 @@ it 'running digest daily sends an email' do match.housing_subsidy_admin_contacts << user.contact perform_enqueued_jobs do - expect { - match.activate! + expect do + match.activate!(user: user) MessageJob.new('daily').perform - }.to change{ + end.to change { ActionMailer::Base.deliveries.size }.by(1) end diff --git a/spec/models/decisions_spec.rb b/spec/models/decisions_spec.rb index a97d84aac..eb9a2799f 100644 --- a/spec/models/decisions_spec.rb +++ b/spec/models/decisions_spec.rb @@ -40,7 +40,7 @@ the_match.hsp_contacts << hsp_user.contact the_match.ssp_contacts << ssp_user.contact - the_match.activate! + the_match.activate!(user: dnd_user) end it 'there are no stalled matches initially' do expect(ClientOpportunityMatch.stalled.count).to be 0 From 8ee665ad8e6f47146fad34236912c8bc996a5d3e Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Mon, 10 Jun 2024 14:55:44 -0400 Subject: [PATCH 02/11] New files --- app/views/reports/parked_clients/index.xlsx.axlsx | 10 ++++++++++ .../20240610132826_add_columns_to_parked_clients.rb | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 app/views/reports/parked_clients/index.xlsx.axlsx create mode 100644 db/migrate/20240610132826_add_columns_to_parked_clients.rb diff --git a/app/views/reports/parked_clients/index.xlsx.axlsx b/app/views/reports/parked_clients/index.xlsx.axlsx new file mode 100644 index 000000000..69d1343d2 --- /dev/null +++ b/app/views/reports/parked_clients/index.xlsx.axlsx @@ -0,0 +1,10 @@ +wb = xlsx_package.workbook +wb.add_worksheet(name: 'Parked-Clients') do |sheet| + sheet.add_row(UnavailableAsCandidateFor.download_columns.keys) + @parked.find_each do |park| + row = UnavailableAsCandidateFor.download_columns.values.map do |col| + col.call(park) + end + sheet.add_row(row) + end +end diff --git a/db/migrate/20240610132826_add_columns_to_parked_clients.rb b/db/migrate/20240610132826_add_columns_to_parked_clients.rb new file mode 100644 index 000000000..8fe3764e6 --- /dev/null +++ b/db/migrate/20240610132826_add_columns_to_parked_clients.rb @@ -0,0 +1,7 @@ +class AddColumnsToParkedClients < ActiveRecord::Migration[7.0] + def change + add_reference :unavailable_as_candidate_fors, :user + add_reference :unavailable_as_candidate_fors, :match + add_column :unavailable_as_candidate_fors, :reason, :string + end +end From 6f52b0eccf123ee0cc203a91f88b3053dd254d1d Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Mon, 10 Jun 2024 15:33:36 -0400 Subject: [PATCH 03/11] Migration to set parked values; expose details on the view --- .../reports/parked_clients_controller.rb | 2 +- app/models/unavailable_as_candidate_for.rb | 4 +-- app/views/reports/parked_clients/index.haml | 25 +++++++------ ...0240610185742_set_parked_client_details.rb | 36 +++++++++++++++++++ db/schema.rb | 2 +- 5 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 db/migrate/20240610185742_set_parked_client_details.rb diff --git a/app/controllers/reports/parked_clients_controller.rb b/app/controllers/reports/parked_clients_controller.rb index 85df1a429..6e73ae106 100644 --- a/app/controllers/reports/parked_clients_controller.rb +++ b/app/controllers/reports/parked_clients_controller.rb @@ -26,7 +26,7 @@ def index merge(client_scope). where(match_route_type: MatchRoutes::Base.active.select(:type)). preload(:user, client: { project_client: :data_source }) - filename = "Parked Clients #{Time.current.to_fs(:db)}" + filename = "Parked Clients #{Time.current.to_fs(:db)}.xlsx" render xlsx: 'index', filename: filename end end diff --git a/app/models/unavailable_as_candidate_for.rb b/app/models/unavailable_as_candidate_for.rb index 1068de944..d6d5c4e00 100644 --- a/app/models/unavailable_as_candidate_for.rb +++ b/app/models/unavailable_as_candidate_for.rb @@ -57,8 +57,8 @@ def self.download_columns 'Route' => ->(parked) { parked.route.title }, 'Parked On' => ->(parked) { parked.created_at.to_date }, 'Parked Until' => ->(parked) { parked.expires_at&.to_date || 'Indefinite' }, - # 'Parked By' => :user_id, - # 'Parked Reason' => :reason, + 'Parked By' => ->(parked) { parked.user&.name_with_email }, + 'Parked Reason' => ->(parked) { parked.reason }, } end end diff --git a/app/views/reports/parked_clients/index.haml b/app/views/reports/parked_clients/index.haml index 9e60c0e24..9fd0a1759 100644 --- a/app/views/reports/parked_clients/index.haml +++ b/app/views/reports/parked_clients/index.haml @@ -1,7 +1,11 @@ = render 'reports/crumbs_operational' -.row - .col-sm-12 - %h1 Currently Parked Clients +.d-flex + %h1 Currently Parked Clients + .ml-auto + = link_to reports_parked_clients_path(format: :xlsx), class: 'btn btn-secondary' do + %i.icon-download2 + Download + - if @routes.count > 1 %ul.nav.nav-tabs{role: :tablist} - @routes.each do |route| @@ -14,13 +18,7 @@ %li.nav-item{role: :presentation} = link_to route_name, reports_parked_clients_path(tab: tab_label), class: "nav-link #{active}" - if @clients.any? - .d-flex - %p= "The following clients have been removed temporarily from matching the #{@route.title}" - .ml-auto - = link_to reports_parked_clients_path(format: :xlsx), class: 'btn btn-secondary' do - %i.icon-download2 - Download - + %p= "The following clients have been removed temporarily from matching the #{@route.title}" %p= page_entries_info @clients .table-responsive .c-card.c.card__flush.mb-4 @@ -31,13 +29,18 @@ %th Last Name %th First Name %th Parked Until + %th Parked By + %th Parked Reason %tbody - @clients.each do |client| + - parked = client.unavailable_as_candidate_fors.merge(UnavailableAsCandidateFor.for_route(@route))&.first %tr %td = link_to_if can_view_all_clients?, client.last_name, client_path(client) %td= link_to_if can_view_all_clients?, client.first_name, client_path(client) - %td= client.unavailable_as_candidate_fors.merge(UnavailableAsCandidateFor.for_route(@route))&.first&.expires_at&.to_date + %td= parked&.expires_at&.to_date + %td= parked&.user&.name_with_email + %td= parked&.reason %p= paginate @clients - else %p No parked clients at this time. diff --git a/db/migrate/20240610185742_set_parked_client_details.rb b/db/migrate/20240610185742_set_parked_client_details.rb new file mode 100644 index 000000000..f64ba4ffa --- /dev/null +++ b/db/migrate/20240610185742_set_parked_client_details.rb @@ -0,0 +1,36 @@ +class SetParkedClientDetails < ActiveRecord::Migration[7.0] + def up + # Set user, match, and reason for parked clients where possible + unavailable_client_ids = UnavailableAsCandidateFor.distinct.pluck(:client_id) + + # Successful matches + MatchRoutes::Base.active.each do |route| + matches = ClientOpportunityMatch.successful.on_route(route).where(client_id: unavailable_client_ids) + client_ids = matches.map(&:client_id) + # not preloading versions since we only want the first version anyway, which would result in a bigger query + parked = UnavailableAsCandidateFor.for_route(route).where(client_id: client_ids).group_by(&:client_id) + matches.each do |match| + parked_for_client = parked[match.client_id] + next unless parked_for_client + + parked_for_client.each do |record| + # NOTE: individual saves, probably fine, but means the migration might be slow + record.update(reason: 'Successful Match', user_id: record.versions&.first&.user_id) + end + end + end + + # Ongoing matches + MatchRoutes::Base.active.each do |route| + matches = ClientOpportunityMatch.active.on_route(route).where(client_id: unavailable_client_ids).group_by(&:client_id) + UnavailableAsCandidateFor.for_route(route).where(client_id: matches.keys).find_each do |record| + record.update(reason: 'Active Match', user_id: record.versions&.first&.user_id, match_id: matches[record.client_id].first.id) + end + end + + # Parked (everyone else) + UnavailableAsCandidateFor.where(reason: nil).find_each do |record| + record.update(reason: 'Parked', user_id: record.versions&.first&.user_id) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index b8777f6f1..2b05aea6f 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: 2024_06_10_132826) do +ActiveRecord::Schema[7.0].define(version: 2024_06_10_185742) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 706e004c3e5b5c7a5feeaf589bd428edcd42ff12 Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Mon, 10 Jun 2024 15:42:06 -0400 Subject: [PATCH 04/11] Remove unused Arel Helper --- app/models/unavailable_as_candidate_for.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/unavailable_as_candidate_for.rb b/app/models/unavailable_as_candidate_for.rb index d6d5c4e00..2c0dea088 100644 --- a/app/models/unavailable_as_candidate_for.rb +++ b/app/models/unavailable_as_candidate_for.rb @@ -8,7 +8,6 @@ # specific match route, existence of a record in this table for a given route # means the client is unavailable. class UnavailableAsCandidateFor < ApplicationRecord - include ArelHelper belongs_to :client belongs_to :route, class_name: 'MatchRoutes::Base', primary_key: :type, foreign_key: :match_route_type belongs_to :user, optional: true From bd37b4da92c663a9d1491a28d4079949d8f34ad7 Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 11 Jun 2024 07:43:39 -0400 Subject: [PATCH 05/11] Update app/models/concerns/availability.rb Co-authored-by: Theron Toomey --- app/models/concerns/availability.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/availability.rb b/app/models/concerns/availability.rb index 97010c091..2bcb5de29 100644 --- a/app/models/concerns/availability.rb +++ b/app/models/concerns/availability.rb @@ -42,9 +42,9 @@ def make_unavailable_in(match_route:, user:, reason:, expires_at: default_unavai route_name = MatchRoutes::Base.route_name_from_route(match_route) # Delete any previous unavailable_as_candidate_fors so we can track the reason for this one - unavailable_as_candidate_fors.where(match_route_type: route_name).destroy_all + unavailable_as_candidate_fors.where(match_route_type: route_name).each(&:desroy!) - unavailable_as_candidate_fors.where(match_route_type: route_name).create( + unavailable_as_candidate_fors.where(match_route_type: route_name).create!( expires_at: expires_at, user_id: user&.id, reason: reason, From 4be53d003e8d339b6947048d6000a55cc07d0232 Mon Sep 17 00:00:00 2001 From: Elliot Date: Tue, 11 Jun 2024 07:45:49 -0400 Subject: [PATCH 06/11] Apply suggestions from code review Co-authored-by: Theron Toomey --- app/models/concerns/availability.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/availability.rb b/app/models/concerns/availability.rb index 2bcb5de29..02e4e18d3 100644 --- a/app/models/concerns/availability.rb +++ b/app/models/concerns/availability.rb @@ -69,7 +69,7 @@ def unavailable( # rubocop:disable Metrics/ParameterLists match: nil ) Client.transaction do - update(available: false) if permanent + update!(available: false) if permanent if cancel_all # Cancel any active matches @@ -80,7 +80,7 @@ def unavailable( # rubocop:disable Metrics/ParameterLists match_id: active_match.id, contact_id: contact_id, ) - opportunity.update(available_candidate: true) + opportunity.update!(available_candidate: true) end # Delete any non-active open matches client_opportunity_matches.open.each(&:delete) From e50cc228c9085f4a468a051bc26d330cdd5f2a51 Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Tue, 11 Jun 2024 07:46:41 -0400 Subject: [PATCH 07/11] DRY up user calculation --- app/models/match_decisions/base.rb | 4 ++++ app/models/match_decisions/confirm_match_success_dnd_staff.rb | 2 +- .../match_decisions/eight/eight_confirm_match_success.rb | 2 +- app/models/match_decisions/five/five_lease_up.rb | 2 +- .../match_decisions/four/confirm_match_success_dnd_staff.rb | 2 +- ...set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb | 2 +- ...nt_housed_date_or_decline_housing_subsidy_administrator.rb | 2 +- app/models/match_decisions/nine/nine_confirm_match_success.rb | 2 +- .../confirm_hsa_accepts_client_decline_dnd_staff.rb | 2 +- .../match_decisions/provider_only/hsa_accepts_client.rb | 2 +- .../match_decisions/seven/confirm_match_success_dnd_staff.rb | 2 +- .../match_decisions/six/confirm_match_success_dnd_staff.rb | 2 +- .../ten/ten_confirm_match_success_dnd_staff.rb | 2 +- 13 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/models/match_decisions/base.rb b/app/models/match_decisions/base.rb index b2798a82a..faf44222f 100644 --- a/app/models/match_decisions/base.rb +++ b/app/models/match_decisions/base.rb @@ -270,6 +270,10 @@ def initialize(decision, dependencies) end delegate :match, to: :decision + private def user + @dependencies.try(:[], :user) + end + def back @decision.uninitialize_decision! end diff --git a/app/models/match_decisions/confirm_match_success_dnd_staff.rb b/app/models/match_decisions/confirm_match_success_dnd_staff.rb index 975b99f06..08ed449bd 100644 --- a/app/models/match_decisions/confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/confirm_match_success_dnd_staff.rb @@ -80,7 +80,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmedDevelopmentOfficer.create_for_match! match - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def rejected diff --git a/app/models/match_decisions/eight/eight_confirm_match_success.rb b/app/models/match_decisions/eight/eight_confirm_match_success.rb index 784351b7c..174d467c8 100644 --- a/app/models/match_decisions/eight/eight_confirm_match_success.rb +++ b/app/models/match_decisions/eight/eight_confirm_match_success.rb @@ -66,7 +66,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def rejected diff --git a/app/models/match_decisions/five/five_lease_up.rb b/app/models/match_decisions/five/five_lease_up.rb index c9dbbf2c3..98dc3e3a5 100644 --- a/app/models/match_decisions/five/five_lease_up.rb +++ b/app/models/match_decisions/five/five_lease_up.rb @@ -68,7 +68,7 @@ def expiration_update end def completed - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def canceled diff --git a/app/models/match_decisions/four/confirm_match_success_dnd_staff.rb b/app/models/match_decisions/four/confirm_match_success_dnd_staff.rb index 1cca629bd..d1f913476 100644 --- a/app/models/match_decisions/four/confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/four/confirm_match_success_dnd_staff.rb @@ -85,7 +85,7 @@ def pending def confirmed Notifications::Four::MatchSuccessConfirmed.create_for_match! match - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def rejected diff --git a/app/models/match_decisions/homeless_set_aside/set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb b/app/models/match_decisions/homeless_set_aside/set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb index c6a9deb7c..8cfb0a67b 100644 --- a/app/models/match_decisions/homeless_set_aside/set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb +++ b/app/models/match_decisions/homeless_set_aside/set_asides_confirm_hsa_accepts_client_decline_dnd_staff.rb @@ -73,7 +73,7 @@ def pending end def decline_overridden - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def decline_overridden_returned diff --git a/app/models/match_decisions/homeless_set_aside/set_asides_record_client_housed_date_or_decline_housing_subsidy_administrator.rb b/app/models/match_decisions/homeless_set_aside/set_asides_record_client_housed_date_or_decline_housing_subsidy_administrator.rb index b96eeef71..0883e3757 100644 --- a/app/models/match_decisions/homeless_set_aside/set_asides_record_client_housed_date_or_decline_housing_subsidy_administrator.rb +++ b/app/models/match_decisions/homeless_set_aside/set_asides_record_client_housed_date_or_decline_housing_subsidy_administrator.rb @@ -120,7 +120,7 @@ def other_clients_canceled end def completed - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def declined diff --git a/app/models/match_decisions/nine/nine_confirm_match_success.rb b/app/models/match_decisions/nine/nine_confirm_match_success.rb index 3a7706e5f..776144ffc 100644 --- a/app/models/match_decisions/nine/nine_confirm_match_success.rb +++ b/app/models/match_decisions/nine/nine_confirm_match_success.rb @@ -65,7 +65,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def rejected diff --git a/app/models/match_decisions/provider_only/confirm_hsa_accepts_client_decline_dnd_staff.rb b/app/models/match_decisions/provider_only/confirm_hsa_accepts_client_decline_dnd_staff.rb index cfecbe867..442bedc27 100644 --- a/app/models/match_decisions/provider_only/confirm_hsa_accepts_client_decline_dnd_staff.rb +++ b/app/models/match_decisions/provider_only/confirm_hsa_accepts_client_decline_dnd_staff.rb @@ -79,7 +79,7 @@ def pending end def decline_overridden - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def decline_overridden_returned diff --git a/app/models/match_decisions/provider_only/hsa_accepts_client.rb b/app/models/match_decisions/provider_only/hsa_accepts_client.rb index e03856c72..013440b13 100644 --- a/app/models/match_decisions/provider_only/hsa_accepts_client.rb +++ b/app/models/match_decisions/provider_only/hsa_accepts_client.rb @@ -101,7 +101,7 @@ def pending def accepted Notifications::ProviderOnly::HsaAcceptsClientSspNotification.create_for_match! match - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def declined diff --git a/app/models/match_decisions/seven/confirm_match_success_dnd_staff.rb b/app/models/match_decisions/seven/confirm_match_success_dnd_staff.rb index 5f7c71ca4..58d973d3c 100644 --- a/app/models/match_decisions/seven/confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/seven/confirm_match_success_dnd_staff.rb @@ -79,7 +79,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def rejected diff --git a/app/models/match_decisions/six/confirm_match_success_dnd_staff.rb b/app/models/match_decisions/six/confirm_match_success_dnd_staff.rb index 6938184b6..4582ae71c 100644 --- a/app/models/match_decisions/six/confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/six/confirm_match_success_dnd_staff.rb @@ -96,7 +96,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def rejected diff --git a/app/models/match_decisions/ten/ten_confirm_match_success_dnd_staff.rb b/app/models/match_decisions/ten/ten_confirm_match_success_dnd_staff.rb index 4d5d3cb2a..ec38e6bac 100644 --- a/app/models/match_decisions/ten/ten_confirm_match_success_dnd_staff.rb +++ b/app/models/match_decisions/ten/ten_confirm_match_success_dnd_staff.rb @@ -86,7 +86,7 @@ def pending def confirmed Notifications::MatchSuccessConfirmed.create_for_match! match - match.succeeded!(user: @dependencies.try(:[], :user)) + match.succeeded!(user: user) end def declined From 36b8c6f8838028c52665a0bdc3c091857d56438f Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Tue, 11 Jun 2024 07:48:20 -0400 Subject: [PATCH 08/11] Use destroy! instead of delete --- app/models/concerns/availability.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/availability.rb b/app/models/concerns/availability.rb index 02e4e18d3..69021cc13 100644 --- a/app/models/concerns/availability.rb +++ b/app/models/concerns/availability.rb @@ -83,7 +83,7 @@ def unavailable( # rubocop:disable Metrics/ParameterLists opportunity.update!(available_candidate: true) end # Delete any non-active open matches - client_opportunity_matches.open.each(&:delete) + client_opportunity_matches.open.each(&:destroy!) # Prevent any new matches for this client # This will re-queue the client once the date is passed make_unavailable_in_all_routes(expires_at: expires_at, user: user, match: match, reason: 'Parked') From ebdb06886b145d1a411eabd58e01e42fcbb6a89d Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Tue, 11 Jun 2024 07:48:54 -0400 Subject: [PATCH 09/11] Use update bang --- app/models/concerns/availability.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/availability.rb b/app/models/concerns/availability.rb index 69021cc13..25f79afe4 100644 --- a/app/models/concerns/availability.rb +++ b/app/models/concerns/availability.rb @@ -100,7 +100,7 @@ def unavailable( # rubocop:disable Metrics/ParameterLists match_id: match.id, contact_id: contact_id, ) - opportunity.update(available_candidate: true) + opportunity.update!(available_candidate: true) # Delete any non-active open matches client_opportunity_matches.on_route(route).proposed.each(&:delete) make_unavailable_in(match_route: route, expires_at: expires_at, user: user, match: match, reason: 'Parked') From cae5f492f3283fd6ee9c10a2013be98017b3eb32 Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Tue, 11 Jun 2024 09:14:26 -0400 Subject: [PATCH 10/11] DRY; plus basic tests --- app/models/client_opportunity_match.rb | 8 +++---- app/models/concerns/availability.rb | 4 ++-- app/models/unavailable_as_candidate_for.rb | 4 ++++ spec/models/client_opportunity_match_spec.rb | 23 +++++++++++++++++++- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/models/client_opportunity_match.rb b/app/models/client_opportunity_match.rb index 15c932de7..70b915c21 100644 --- a/app/models/client_opportunity_match.rb +++ b/app/models/client_opportunity_match.rb @@ -646,7 +646,7 @@ def restrictions_on_reopening def reopen!(contact, user: nil) self.class.transaction do - client.make_unavailable_in(match_route: match_route, expires_at: nil, user: user, match: self, reason: 'Active Match') + client.make_unavailable_in(match_route: match_route, expires_at: nil, user: user, match: self, reason: UnavailableAsCandidateFor::ACTIVE_MATCH_TEXT) update(closed: false, active: true, closed_reason: nil) current_decision.update(status: :pending) MatchEvents::Reopened.create(match_id: id, contact_id: contact.id) @@ -661,7 +661,7 @@ def reopen!(contact, user: nil) def activate!(touch_referral_event: true, user: nil) self.class.transaction do update!(active: true) - client.make_unavailable_in(match_route: match_route, expires_at: nil, user: user, match: self, reason: 'Active Match') if match_route.should_prevent_multiple_matches_per_client + client.make_unavailable_in(match_route: match_route, expires_at: nil, user: user, match: self, reason: UnavailableAsCandidateFor::ACTIVE_MATCH_TEXT) if match_route.should_prevent_multiple_matches_per_client opportunity.update(available_candidate: false) add_default_contacts! requirements_with_inherited.each { |req| req.apply_to_match(self) } @@ -750,10 +750,10 @@ def succeeded!(touch_referral_event: true, user: nil) end client.update available: false # Prevent matching on any route - client.make_unavailable_in_all_routes(user: user, match: self, reason: 'Successful Match') + client.make_unavailable_in_all_routes(user: user, match: self, reason: UnavailableAsCandidateFor::SUCCESSFUL_MATCH_TEXT) else # Prevent matching on this route again - client.make_unavailable_in(match_route: route, user: user, match: self, reason: 'Successful Match') + client.make_unavailable_in(match_route: route, user: user, match: self, reason: UnavailableAsCandidateFor::SUCCESSFUL_MATCH_TEXT) end # Cleanup other matches on the same opportunity diff --git a/app/models/concerns/availability.rb b/app/models/concerns/availability.rb index 25f79afe4..0f19e1554 100644 --- a/app/models/concerns/availability.rb +++ b/app/models/concerns/availability.rb @@ -86,7 +86,7 @@ def unavailable( # rubocop:disable Metrics/ParameterLists client_opportunity_matches.open.each(&:destroy!) # Prevent any new matches for this client # This will re-queue the client once the date is passed - make_unavailable_in_all_routes(expires_at: expires_at, user: user, match: match, reason: 'Parked') + make_unavailable_in_all_routes(expires_at: expires_at, user: user, match: match, reason: UnavailableAsCandidateFor::PARKED_TEXT) end if cancel_specific @@ -103,7 +103,7 @@ def unavailable( # rubocop:disable Metrics/ParameterLists opportunity.update!(available_candidate: true) # Delete any non-active open matches client_opportunity_matches.on_route(route).proposed.each(&:delete) - make_unavailable_in(match_route: route, expires_at: expires_at, user: user, match: match, reason: 'Parked') + make_unavailable_in(match_route: route, expires_at: expires_at, user: user, match: match, reason: UnavailableAsCandidateFor::PARKED_TEXT) end end Matching::RunEngineJob.perform_later diff --git a/app/models/unavailable_as_candidate_for.rb b/app/models/unavailable_as_candidate_for.rb index 2c0dea088..3258355ca 100644 --- a/app/models/unavailable_as_candidate_for.rb +++ b/app/models/unavailable_as_candidate_for.rb @@ -14,6 +14,10 @@ class UnavailableAsCandidateFor < ApplicationRecord belongs_to :match, class_name: 'ClientOpportunityMatch', optional: true has_paper_trail + ACTIVE_MATCH_TEXT = 'Active Match'.freeze + SUCCESSFUL_MATCH_TEXT = 'Successful Match'.freeze + PARKED_TEXT = 'Parked'.freeze + scope :for_route, ->(route) do route_name = MatchRoutes::Base.route_name_from_route(route) where(match_route_type: route_name) diff --git a/spec/models/client_opportunity_match_spec.rb b/spec/models/client_opportunity_match_spec.rb index 1919805e5..b47c809d7 100644 --- a/spec/models/client_opportunity_match_spec.rb +++ b/spec/models/client_opportunity_match_spec.rb @@ -4,7 +4,7 @@ RSpec.describe ClientOpportunityMatch, type: :model do let!(:match) { create :client_opportunity_match, active: false } let(:priority) { create :priority_vispdat_priority } - let(:default_route) { create :default_route, match_prioritization: priority } + let(:default_route) { create :default_route, match_prioritization: priority, should_prevent_multiple_matches_per_client: true } let(:provider_route) { create :provider_route, match_prioritization: priority } describe 'Match initiation on the default route' do describe 'when the initial user has email schedule set to immediate' do @@ -53,6 +53,27 @@ }.by(1) end end + it 'generates an unavailable for on the route on activation' do + match.activate!(user: user) + aggregate_failures do + expect(UnavailableAsCandidateFor.count).to eq(1) + expect(UnavailableAsCandidateFor.first.route).to eq(default_route) + expect(UnavailableAsCandidateFor.first.reason).to eq(UnavailableAsCandidateFor::ACTIVE_MATCH_TEXT) + expect(UnavailableAsCandidateFor.first.user_id).to eq(user.id) + expect(UnavailableAsCandidateFor.first.match_id).to eq(match.id) + end + end + + it 'generates an unavailable for on the route on success' do + match.succeeded!(user: user) + aggregate_failures do + expect(UnavailableAsCandidateFor.count).to eq(MatchRoutes::Base.all_routes.count) + expect(UnavailableAsCandidateFor.pluck(:match_route_type).sort).to eq(MatchRoutes::Base.all_routes.map(&:name).sort) + expect(UnavailableAsCandidateFor.pluck(:reason).uniq).to eq([UnavailableAsCandidateFor::SUCCESSFUL_MATCH_TEXT]) + expect(UnavailableAsCandidateFor.pluck(:user_id).uniq).to eq([user.id]) + expect(UnavailableAsCandidateFor.pluck(:match_id).uniq).to eq([match.id]) + end + end end describe 'when the initial user has email schedule set to daily' do From 91a49405ac9e1506ca6b639748d5ba6535c7b428 Mon Sep 17 00:00:00 2001 From: Elliot Anders Date: Tue, 11 Jun 2024 09:25:10 -0400 Subject: [PATCH 11/11] Possible fix for id drift in tests --- spec/models/client_opportunity_match_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/client_opportunity_match_spec.rb b/spec/models/client_opportunity_match_spec.rb index b47c809d7..0d9c0b127 100644 --- a/spec/models/client_opportunity_match_spec.rb +++ b/spec/models/client_opportunity_match_spec.rb @@ -57,7 +57,7 @@ match.activate!(user: user) aggregate_failures do expect(UnavailableAsCandidateFor.count).to eq(1) - expect(UnavailableAsCandidateFor.first.route).to eq(default_route) + expect(UnavailableAsCandidateFor.first.match_route_type).to eq(default_route.class.name) expect(UnavailableAsCandidateFor.first.reason).to eq(UnavailableAsCandidateFor::ACTIVE_MATCH_TEXT) expect(UnavailableAsCandidateFor.first.user_id).to eq(user.id) expect(UnavailableAsCandidateFor.first.match_id).to eq(match.id)