From 0f2a2cbb0a2d40ca3e76968767ced78b93626798 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 6 Feb 2025 15:06:35 -0500 Subject: [PATCH] CE Transfer assessments (#4673) * Additional default translations for known assessment keys * Set IdentifiedPathwaysVersionFourTransfer where appropriate * Adjustments for Q5 and sync logic * Test fix * Pathways 2024 adjustments (#5049) * Updated Pathways Tie Breaker Date * Set first date of service * Update language for tie breaker date * Allow CAS Readiness page to be loaded sync for development * Enforce desc sort on CE assessments * Additional housing barriers * Eviction from pathways or transfer * Calculate transfer score from assessment result * Tie breaker for 2024 transfer assessment * WIP: note to adjust family pathways assessment score calculation * Distinction between pathways (individual) and family pathways; pull days homeless from assessment if available; use overall days homeless for assessment score for family pathways * Additional overall calculations for days homeless; preload source client data source; don't try to get phone numbers from HMIS if no HMIS exists --------- Co-authored-by: Dave G <149399758+dtgreiner@users.noreply.github.com> --- app/models/concerns/cas_client_data.rb | 3 +- .../cas_project_client_calculator/boston.rb | 108 +++++++++++++++++- .../cas_project_client_calculator/default.rb | 5 + app/models/grda_warehouse/hud/assessment.rb | 8 ++ .../grda_warehouse/hud/assessment_question.rb | 38 +++++- app/models/grda_warehouse/hud/client.rb | 8 +- .../tasks/push_clients_to_cas.rb | 23 ++-- app/models/translation.rb | 10 ++ app/views/clients/cas_readiness/edit.haml | 10 +- .../clients/rollup/_assessments.html.haml | 2 +- 10 files changed, 185 insertions(+), 30 deletions(-) diff --git a/app/models/concerns/cas_client_data.rb b/app/models/concerns/cas_client_data.rb index 48064e6ca5..967b971685 100644 --- a/app/models/concerns/cas_client_data.rb +++ b/app/models/concerns/cas_client_data.rb @@ -881,6 +881,7 @@ def cas_assessment_collected_at :additional_homeless_nights_unsheltered, :calculated_homeless_nights_sheltered, :calculated_homeless_nights_unsheltered, - :total_homeless_nights_sheltered + :total_homeless_nights_sheltered, + :psh_required end end diff --git a/app/models/grda_warehouse/cas_project_client_calculator/boston.rb b/app/models/grda_warehouse/cas_project_client_calculator/boston.rb index 4975e2ef4c..f4e3e156ef 100644 --- a/app/models/grda_warehouse/cas_project_client_calculator/boston.rb +++ b/app/models/grda_warehouse/cas_project_client_calculator/boston.rb @@ -33,6 +33,10 @@ def value_for_cas_project_client(client:, column:) end end + def handles_days_homeless? + true + end + def unrelated_columns [ :vispdat_score, @@ -61,7 +65,7 @@ def unrelated_columns cas_assessment_collected_at: 'Date the assessment was collected', # note this is really just assessment_collected_at majority_sheltered: 'Most recent current living situation was sheltered', assessment_score_for_cas: 'Days homeless in the past 3 years for pathways, score for transfer assessments', - tie_breaker_date: 'Date pathways was collected, or Financial Assistance End Date for transfer assessments', + tie_breaker_date: 'Date pathways was collected for Pathways 2023, First Date Homeless for Pathways 2024, or Financial Assistance End Date for Transfer Assessments', financial_assistance_end_date: 'Latest Date Eligible for Financial Assistance response from the most recent pathways assessment', assessor_first_name: 'First name of the user who completed the most recent pathways assessment', assessor_last_name: 'Last name of the user who completed the most recent pathways assessment', @@ -73,6 +77,7 @@ def unrelated_columns days_homeless_for_vispdat_prioritization: 'Unused', hiv_positive: 'HIV/AIDS response from the most recent pathways assessment', meth_production_conviction: 'Meth production response from the most recent pathways assessment', + lifetime_sex_offender: 'Registered sex offender (level 1,2,3) - lifetime registration (SORI)', requires_wheelchair_accessibility: 'Does the client need a wheelchair accessible unit response from the most recent pathways assessment', income_maximization_assistance_requested: 'Did the client request income maximization services response from the most recent pathways assessment', sro_ok: 'Is the client ok with an SRO response from the most recent pathways assessment', @@ -94,7 +99,6 @@ def unrelated_columns hiv_positive: 'c_housing_HIV', income_maximization_assistance_requested: 'c_interest_income_max', sro_ok: 'c_singleadult_sro', - evicted: 'c_pathways_barrier_eviction', rrh_desired: 'c_interested_rrh', housing_barrier: 'c_pathways_barriers_yn', }.freeze @@ -143,6 +147,13 @@ def unrelated_columns :calculated_homeless_nights_unsheltered, :total_homeless_nights_sheltered, :total_homeless_nights_unsheltered, + :date_of_first_service, + :psh_required, + :meth_production_conviction, + :lifetime_sex_offender, + :evicted, + :days_homeless, + :hmis_days_homeless_all_time, ] end # memoize :pathways_questions @@ -186,6 +197,23 @@ def most_recent_transfer_assessment_for_destination(client) nil end + private def lifetime_sex_offender(client) + most_recent_pathways_or_transfer(client). + question_matching_requirement('c_transfer_barrier_SORI', '1').present? + end + + private def evicted(client) + evicted = most_recent_pathways_or_transfer(client).question_matching_requirement('c_pathways_barrier_meth', '1').present? + return true if evicted + + evicted = most_recent_pathways_or_transfer(client). + question_matching_requirement('c_transfer_barrier_PHAterm', '1').present? + return true if evicted + + # Otherwise unknown + nil + end + private def service_need(client) need = most_recent_pathways_or_transfer(client).question_matching_requirement('c_pathways_service_indicators', '1').present? return true if need @@ -321,6 +349,18 @@ def days_homeless_in_last_three_years_cached(client) pre_calculated_days end + # Overrides the usual calculation for first date homeless if available + # If the question doesn't exist on the assessment or is empty, use the usual definition + private def date_of_first_service(client) + field_name = 'c_pathways_first_date_homeless' + answer = most_recent_pathways_or_transfer(client). + question_matching_requirement(field_name)&.AssessmentAnswer + + return client.date_of_first_service if answer.blank? + + answer.to_date + end + # If a client has more than 548 self-reported days (combination of sheltered and unsheltered) # and does not have a verification uploaded, count unsheltered days first, then count sheltered days UP TO 548. # If the self reported days are verified, use the provided amounts. @@ -396,12 +436,46 @@ def total_homeless_nights_sheltered(client) question_matching_requirement('c_pathways_nights_sheltered_warehouse_added_total')&.AssessmentAnswer.to_i || 0 end + # all-time days homeless + def days_homeless(client) + overall_nights_homeless(client) + end + + # this seems to be calculated many different ways + def hmis_days_homeless_all_time(client) + overall_nights_homeless(client) + end + + private def overall_nights_homeless(client) + most_recent_pathways_or_transfer(client). + question_matching_requirement('c_new_boston_homeless_nights_total')&.AssessmentAnswer.to_i || + client.days_homeless + end + private def default_shelter_agency_contacts(client) contact_emails = client.client_contacts.shelter_agency_contacts.where.not(email: nil).pluck(:email) contact_emails << client.source_assessments.max_by(&:assessment_date)&.user&.user_email contact_emails.compact.uniq end + # 0 = No PSH + # 1 = PSH required + # 2 = Either + # Default to either if we don't have an answer + # CAS uses `yes`, `no`, `maybe` strings + private def psh_required(client) + value = most_recent_pathways_or_transfer(client). + question_matching_requirement('c_rrh_transfer_needs_subsidized_housing_resource')&.AssessmentAnswer.to_i || 2 + case value + when 0 + 'no' + when 1 + 'yes' + else + 'maybe' + end + end + private def contact_info_for_rrh_assessment(client) client.client_contacts.case_managers.map(&:full_address).join("\n\n") end @@ -419,6 +493,7 @@ def total_homeless_nights_sheltered(client) 1 => 'IdentifiedPathwaysVersionThreePathways', 2 => 'IdentifiedPathwaysVersionThreeTransfer', 3 => 'IdentifiedPathwaysVersionFourPathways', + 4 => 'IdentifiedPathwaysVersionFourTransfer', }[value.to_i] || 'IdentifiedClientAssessment' end @@ -447,21 +522,42 @@ def total_homeless_nights_sheltered(client) question_matching_requirement('c_latest_date_financial_assistance_eligibility_rrh')&.AssessmentAnswer end + # For 2024/2025 score calculations are: + # Individual Pathways Assessment: days homeless in the past 3 years, prefer Pathways answer clamped to 1,096, use + # days in the past three years from the warehouse if not available. + # Family Pathways Assessment: overall days homeless (all time), prefer Pathways answer, use client.days_homeless if + # not available. + # Transfer Assessment: use assessment score. private def assessment_score_for_cas(client) case cas_assessment_name(client) when 'IdentifiedPathwaysVersionThreePathways', 'IdentifiedPathwaysVersionFourPathways' - days_homeless_in_last_three_years_cached(client) - when 'IdentifiedPathwaysVersionThreeTransfer' + if most_recent_pathways_or_transfer(client).family_pathways_2024? + # Family + overall_days_homeless(client) + else + # Individual + days_homeless_in_last_three_years_cached(client) + end + + # all time homeless days (no cap on total, but self-report limited to 548 if no verification) + # Also need a mechanism to identify family Pathways assessments/AssessmentQuestions + when 'IdentifiedPathwaysVersionThreeTransfer', 'IdentifiedPathwaysVersionFourTransfer' assessment_score(client) end end + # Various tie-breaker dates used for prioritization in CAS when all else is equal + # For Pathways V3, use the date the assessment was collected + # For the V3 Transfer assessment, use the financial assistance end date + # For Pathways V4, use the first date of homelessness private def tie_breaker_date(client) case cas_assessment_name(client) - when 'IdentifiedPathwaysVersionThreePathways', 'IdentifiedPathwaysVersionFourPathways' + when 'IdentifiedPathwaysVersionThreePathways' cas_assessment_collected_at(client) - when 'IdentifiedPathwaysVersionThreeTransfer' + when 'IdentifiedPathwaysVersionThreeTransfer', 'IdentifiedPathwaysVersionFourTransfer' financial_assistance_end_date(client) + when 'IdentifiedPathwaysVersionFourPathways' + date_of_first_service(client) end end diff --git a/app/models/grda_warehouse/cas_project_client_calculator/default.rb b/app/models/grda_warehouse/cas_project_client_calculator/default.rb index ea9c35696b..df5396bd86 100644 --- a/app/models/grda_warehouse/cas_project_client_calculator/default.rb +++ b/app/models/grda_warehouse/cas_project_client_calculator/default.rb @@ -15,6 +15,11 @@ def value_for_cas_project_client(client:, column:) client.send(column) end + # Defer calculation of `days_homeless` to PushClientsToCas + def handles_days_homeless? + false + end + def description_for_column(column) custom_descriptions[column].presence || GrdaWarehouse::Hud::Client.cas_columns_data.dig(column, :description) end diff --git a/app/models/grda_warehouse/hud/assessment.rb b/app/models/grda_warehouse/hud/assessment.rb index 6563f737ba..a126dbe16d 100644 --- a/app/models/grda_warehouse/hud/assessment.rb +++ b/app/models/grda_warehouse/hud/assessment.rb @@ -63,6 +63,10 @@ class Assessment < Base where(AssessmentID: GrdaWarehouse::Hud::AssessmentQuestion.transfer.select(:AssessmentID)) end + scope :family_pathways, -> do + where(AssessmentID: GrdaWarehouse::Hud::AssessmentQuestion.family_pathways.select(:AssessmentID)) + end + def answer(question) assessment_questions.find_by(assessment_question: question.to_s)&.assessment_answer end @@ -105,5 +109,9 @@ def name def pathways? assessment_questions.any?(&:pathways?) end + + def family_pathways_2024? + name == 'Family Pathways 2024' + end end end diff --git a/app/models/grda_warehouse/hud/assessment_question.rb b/app/models/grda_warehouse/hud/assessment_question.rb index 2920dbad97..62b331c06b 100644 --- a/app/models/grda_warehouse/hud/assessment_question.rb +++ b/app/models/grda_warehouse/hud/assessment_question.rb @@ -47,15 +47,18 @@ class AssessmentQuestion < Base pathways_or_rrh. joins(:lookup). merge(GrdaWarehouse::AssessmentAnswerLookup.where(response_text: pathways_titles)) - - # Temporary solution until we have the c_housing_assessment_name question in the 2024 pathways assessment - # where(AssessmentQuestion: [:c_housing_assessment_name, :c_pathways_barriers_yn]) end scope :transfer, -> do pathways_or_rrh. joins(:lookup). - merge(GrdaWarehouse::AssessmentAnswerLookup.where(response_text: 'RRH-PSH Transfer')) + merge(GrdaWarehouse::AssessmentAnswerLookup.where(response_text: transfer_titles)) + end + + scope :family_pathways, -> do + pathways_or_rrh. + joins(:lookup). + merge(GrdaWarehouse::AssessmentAnswerLookup.where(response_text: family_pathways_titles)) end # NOTE: you probably want to join/preload :lookup @@ -71,8 +74,6 @@ def default_response_text(answer) end def pathways? - # FIXME: this is temporary until we have a more permanent solution - # self.AssessmentQuestion.to_s == 'c_pathways_barriers_yn' self.AssessmentQuestion.to_s == 'c_housing_assessment_name' && human_readable.in?(self.class.pathways_titles) end @@ -83,6 +84,19 @@ def self.pathways_titles ] end + def self.transfer_titles + [ + 'RRH-PSH Transfer', + 'RRH-PSH Transfer 2024', + ] + end + + def self.family_pathways_titles + [ + 'Family Pathways 2024', + ] + end + private def pathways_question? assessment_question == 'c_housing_assessment_name' end @@ -91,11 +105,17 @@ def assessment_name return 'Pathways 2024' if pathways_2024? return 'Pathways 2021' if pathways_2021? return 'RRH-PSH Transfer' if transfer_2021? + return 'RRH-PSH Transfer 2024' if transfer_2024? + return 'Family Pathways 2024' if family_pathways_2024? # unknown from assessment questions nil end + def family_pathways_2024? + lookup&.response_text == 'Family Pathways 2024' + end + def pathways_2024? return nil unless pathways_question? @@ -113,5 +133,11 @@ def transfer_2021? lookup&.response_text == 'RRH-PSH Transfer' end + + def transfer_2024? + return nil unless pathways_question? + + lookup&.response_text == 'RRH-PSH Transfer 2024' + end end end diff --git a/app/models/grda_warehouse/hud/client.rb b/app/models/grda_warehouse/hud/client.rb index 6eb34424b2..5f14564cec 100644 --- a/app/models/grda_warehouse/hud/client.rb +++ b/app/models/grda_warehouse/hud/client.rb @@ -1357,7 +1357,7 @@ def email return source_clients.map(&:email).reject(&:blank?).first if destination? # Look for value from OP HMIS - value = most_recent_email_hmis if HmisEnforcement.hmis_enabled? + value = most_recent_email_hmis if HmisEnforcement.hmis_enabled? && data_source&.hmis? # Look for value from other HMIS integrations value ||= hmis_client_response['Email'] if hmis_client_response.present? value ||= hmis_client.processed_fields['email'] if hmis_client&.processed_fields @@ -1368,7 +1368,7 @@ def home_phone # Fetch the data from the source clients if we are a destination client return source_clients.map(&:home_phone).reject(&:blank?).first if destination? - value = most_recent_home_phone_hmis if HmisEnforcement.hmis_enabled? + value = most_recent_home_phone_hmis if HmisEnforcement.hmis_enabled? && data_source&.hmis? value ||= hmis_client_response['HomePhone'] if hmis_client_response.present? value end @@ -1377,7 +1377,7 @@ def cell_phone # Fetch the data from the source clients if we are a destination client return source_clients.map(&:cell_phone).reject(&:blank?).first if destination? - value = most_recent_cell_or_other_phone_hmis if HmisEnforcement.hmis_enabled? + value = most_recent_cell_or_other_phone_hmis if HmisEnforcement.hmis_enabled? && data_source&.hmis? value ||= hmis_client_response['CellPhone'] if hmis_client_response.present? value ||= hmis_client.processed_fields['phone'] if hmis_client&.processed_fields value @@ -1387,7 +1387,7 @@ def work_phone # Fetch the data from the source clients if we are a destination client return source_clients.map(&:work_phone).reject(&:blank?).first if destination? - value = most_recent_work_or_school_phone_hmis if HmisEnforcement.hmis_enabled? + value = most_recent_work_or_school_phone_hmis if HmisEnforcement.hmis_enabled? && data_source&.hmis? return value if value return unless hmis_client_response.present? diff --git a/app/models/grda_warehouse/tasks/push_clients_to_cas.rb b/app/models/grda_warehouse/tasks/push_clients_to_cas.rb index 217d219991..8cdde2e6e4 100644 --- a/app/models/grda_warehouse/tasks/push_clients_to_cas.rb +++ b/app/models/grda_warehouse/tasks/push_clients_to_cas.rb @@ -50,6 +50,7 @@ def sync! source_clients: [ :most_recent_current_living_situation, :most_recent_tc_hat, + :data_source, { most_recent_pathways_or_rrh_assessment: [ :assessment_questions, @@ -76,11 +77,14 @@ def sync! project_client = project_clients[client.id] || CasAccess::ProjectClient.new(data_source_id: data_source.id, id_in_data_source: client.id) project_client.assign_attributes(attributes_for_cas_project_client(client)) - case GrdaWarehouse::Config.get(:cas_days_homeless_source) - when 'days_homeless_plus_overrides' - project_client.days_homeless = client.processed_service_history&.days_homeless_plus_overrides || client.days_homeless - else - project_client.days_homeless = client.days_homeless + # The Boston calculator handles days homeless natively + unless calculator_instance.handles_days_homeless? + case GrdaWarehouse::Config.get(:cas_days_homeless_source) + when 'days_homeless_plus_overrides' + project_client.days_homeless = client.processed_service_history&.days_homeless_plus_overrides || client.days_homeless + else + project_client.days_homeless = client.days_homeless + end end project_client.calculated_last_homeless_night = client.date_of_last_homeless_service @@ -109,8 +113,8 @@ def sync! project_client.needs_update = true to_update << project_client end - to_insert = to_update.select { |c| c.id.blank? } - to_upsert = to_update.select { |c| c.id.present? } + to_insert = to_update.select { |c| c.id.blank? }.uniq + to_upsert = to_update.select { |c| c.id.present? }.uniq CasAccess::ProjectClient.import!(to_upsert, on_duplicate_key_update: update_columns) if to_upsert.present? CasAccess::ProjectClient.import!(update_columns, to_insert) if to_insert.present? @@ -290,12 +294,15 @@ def client_source total_homeless_nights_sheltered: :total_homeless_nights_sheltered, service_need: :service_need, housing_barrier: :housing_barrier, + psh_required: :psh_required, } end private def attributes_for_cas_project_client(client) {}.tap do |options| - project_client_columns.map do |destination, source| + columns = project_client_columns + columns[:days_homeless] = :days_homeless if calculator_instance.handles_days_homeless? + columns.map do |destination, source| # puts "Processing: #{destination} from: #{source}" options[destination] = calculator_instance.value_for_cas_project_client(client: client, column: source) end diff --git a/app/models/translation.rb b/app/models/translation.rb index 2ac7110523..538d922de9 100644 --- a/app/models/translation.rb +++ b/app/models/translation.rb @@ -1847,6 +1847,16 @@ def self.default_translations 'c_pathways_service_indicators_shelter_restriction' => 'I\'ve faced indefinite restrictions and a history of restrictions from area shelters', 'c_share_permission' => 'Permission to Share Your Information with Partner Agencies', 'c_singleadult_sro' => 'If you are a single adult, would you consider living in a single room occupancy (SRO)?', + 'c_transfer_barrier_meth' => 'Have been convicted or found guilty of producing methamphetamine on subsidized properties', + 'c_transfer_barrier_PHAterm' => 'Have been evicted from a BHA development or have had a BHA voucher terminated within the last three years', + 'c_transfer_barrier_SORI' => 'Registered sex offender (level 1,2,3) - lifetime registration (SORI)', + 'c_rrh_transfer_daily_life_functions' => 'Does client have a physical or mental impairment that substantially limits one or more daily life functions? Daily life functions include: obtaining food/eating, sleeping, physical movement, caring for one’s personal hygiene, and communicating.', + 'c_rrh_transfer_addiction_history' => 'Does the client have a history of substance use disorder that prevents them from living independently without support services?', + 'c_rrh_transfer_federal_benefits' => 'Does the client qualify for federal benefits?', + 'c_rrh_transfer_eviction_history' => 'Does the client have one eviction within the last 5 years?', + 'c_rrh_transfer_eviction_history_disabilty_cause' => 'Were any previous evictions due to a disability or substance use disorder', + 'c_transfer_rrh_enrollment_moves' => 'How many times has the client moved while enrolled in rapid Re-housing?', + 'c_latest_date_financial_assistance_eligibility_rrh' => 'Enter date for last day of financial assistance', } end end diff --git a/app/views/clients/cas_readiness/edit.haml b/app/views/clients/cas_readiness/edit.haml index 719792f4f0..98f1ef8223 100644 --- a/app/views/clients/cas_readiness/edit.haml +++ b/app/views/clients/cas_readiness/edit.haml @@ -10,7 +10,9 @@ %p The following information is used exclusively for matching clients to vacancies in CAS. It is not used for federal reporting. - - = render 'background_render', url: render_content_client_cas_readiness_path, fetch_params: { client_id: @client.id } do - .rollup - .rollup-container.well + - if params[:render_inline] == "1" && Rails.env.development? + = render 'render_content' + - else + = render 'background_render', url: render_content_client_cas_readiness_path, fetch_params: { client_id: @client.id } do + .rollup + .rollup-container.well diff --git a/app/views/clients/rollup/_assessments.html.haml b/app/views/clients/rollup/_assessments.html.haml index f366f15266..9eaf9ed5cf 100644 --- a/app/views/clients/rollup/_assessments.html.haml +++ b/app/views/clients/rollup/_assessments.html.haml @@ -5,7 +5,7 @@ - if RailsDrivers.loaded.include?(:eccovia_data) - eccovia_assessments = @client.source_eccovia_assessments - if @client.source_assessments.exists? - - ce_assessments = @client.source_assessments.preload(:assessment_questions).to_a + - ce_assessments = @client.source_assessments.order(assessment_date: :desc).preload(:assessment_questions).to_a - custom_hmis_assessments = [] - if HmisEnforcement.hmis_enabled? - custom_hmis_assessments = @client.hmis_source_custom_assessments.order(assessment_date: :desc).preload(:user, enrollment: [:data_source, { project: :warehouse_project }], form_processor: :definition).to_a