Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions drivers/hmis/app/graphql/types/hmis_schema/ce_referral.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def self.authorized?(object, ctx)
summary_field :client_id, ID, null: false
summary_field :client_name, String, null: true, description: 'The name of the referred client. Always available to those who can view the full referral, even without full client record access.'
summary_field :created_at, GraphQL::Types::ISO8601DateTime, null: false
summary_field :source_enrollment_id, ID, null: true
summary_field :source_enrollment_id, ID, null: true # may be null for VSP referrals or if the source enrollment was deleted
# source_project_name is resolved separately from source_enrollment as a summary field to minimize data exposure
summary_field :source_project_name, String, null: true
# Resolve project fields separately, instead of on the project schema object, in case user can't view the project
Expand Down Expand Up @@ -213,11 +213,11 @@ def target_enrollment
end

def source_enrollment
return unless object.source_enrollment_id
return unless object.source_enrollment_id # May be null for VSP referrals or if the source enrollment was deleted

# Resolve source Enrollment without checking viewable_by.This resolves as type CeReferralSourceEnrollment, so it only exposes limited data from the Enrollment
# Resolve source Enrollment without checking viewable_by. This resolves as type CeReferralSourceEnrollment, so it only exposes limited data from the Enrollment
enrollment = load_ar_association(object, :source_enrollment)
return unless enrollment # May be missing if source enrollment was deleted and reference was not cleaned up
return unless enrollment # May be missing if source enrollment was deleted and reference was not cleaned up (cleanup added with #8539)

# Not passing definition_identifiers because we don't need to resolve assessment data in this context (for now)
OpenStruct.new(enrollment: enrollment, definition_identifiers: [])
Expand Down
4 changes: 4 additions & 0 deletions drivers/hmis/app/models/hmis/ce/referral.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ def self.sort_by_option(option)
validate :ce_template
validate :consistent_data_source
validate :consistent_project
# Direct referrals require a source enrollment to identify which project sent the referral.
# Without a source enrollment, the referral would "float" and not appear on the source project's
# outgoing referrals list, making it difficult to track and manage.
validates :source_enrollment_id, presence: true, if: -> { referral_origin == DIRECT_SEND_ORIGIN }

# When referral status changes, its CustomReferralStatus (user-facing status) should also be updated.
# See ReferralMessageHandler for example.
Expand Down
3 changes: 2 additions & 1 deletion drivers/hmis/app/models/hmis/ce/referral_ce_event_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def initialize(referral)

def create_ce_event(message)
enrollment = referral.source_enrollment
raise 'Referral does not have a source enrollment' unless enrollment.present?
# Skip CE Event creation if source Enrollment is missing. (Expected for VSP referrals or if the source enrollment was deleted)
return unless enrollment

# If the referral already has a CE event, return early and don't raise
return if referral.ce_event.present?
Expand Down
23 changes: 23 additions & 0 deletions drivers/hmis/app/models/hmis/hud/enrollment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ class Hmis::Hud::Enrollment < Hmis::Hud::Base

accepts_nested_attributes_for :move_in_addresses, allow_destroy: true

before_destroy :check_active_outgoing_direct_referrals
before_destroy :nullify_outgoing_waitlist_referrals
before_destroy :destroy_outgoing_direct_referrals
Comment on lines +115 to +117

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To do - add handling for destroying target enrollment

before_validation :set_hud_project_id_from_project_pk_unless_wip, if: :project_pk_changed?
def set_hud_project_id_from_project_pk_unless_wip
return unless project_id
Expand Down Expand Up @@ -538,6 +541,26 @@ def build_synthetic_intake_assessment
assessment
end

# Prevents deletion of an enrollment that has active outgoing direct referrals.
private def check_active_outgoing_direct_referrals
return unless outgoing_ce_referrals.originated_from_direct_send.active.exists?

raise ActiveRecord::DeleteRestrictionError, 'active outgoing direct referrals'
end

# Nullifies source_enrollment_id on waitlist-based referrals when enrollment is deleted.
# Waitlist-based referrals can exist without as source enrollment since they're already resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs ticket: support changing/adding source enrollment to referral

private def nullify_outgoing_waitlist_referrals
outgoing_ce_referrals.originated_from_waitlist.each { |referral| referral.update!(source_enrollment_id: nil) }
end

# Destroys all direct send referrals when enrollment is deleted.
# Direct send referrals require the source enrollment to identify which project sent them.
# Without the source enrollment, they would "float" and not appear on the source project's outgoing referrals list.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not as compelling, revisit this one.. its more about the target project no?

private def destroy_outgoing_direct_referrals
outgoing_ce_referrals.originated_from_direct_send.each(&:destroy!)
end

# When submitting a new_client_enrollment form, we validate the client too, with the same validation contexts
private def client_is_valid
return unless client.present?
Expand Down
118 changes: 91 additions & 27 deletions drivers/hmis/spec/models/hmis/hud/enrollment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,6 @@
require_relative '../../../support/hmis_base_setup'

RSpec.describe Hmis::Hud::Enrollment, type: :model do
before(:all) do
cleanup_test_environment
end
after(:all) do
cleanup_test_environment
end

include_context 'hmis base setup'

it 'detects date conflicts' do
Expand Down Expand Up @@ -82,33 +75,26 @@
end
end

describe 'in progress enrollments' do
let!(:enrollment) { build(:hmis_hud_enrollment) }
before(:each) do
enrollment.save_in_progress!
end
end

describe 'saved enrollments' do
let!(:enrollment) { create(:hmis_hud_enrollment) }
let!(:enrollment) { create(:hmis_hud_enrollment, data_source: ds1) }

before(:each) do
create(:hmis_hud_exit, data_source: enrollment.data_source, enrollment: enrollment, client: enrollment.client)
create(:hmis_hud_service, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_hud_event, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_income_benefit, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_disability, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_health_and_dv, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_current_living_situation, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_hud_assessment, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_employment_education, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_youth_education_status, data_source: enrollment.data_source, enrollment: enrollment)
create(:hmis_hud_exit, data_source: ds1, enrollment: enrollment, client: enrollment.client)
create(:hmis_hud_service, data_source: ds1, enrollment: enrollment)
create(:hmis_hud_event, data_source: ds1, enrollment: enrollment)
create(:hmis_income_benefit, data_source: ds1, enrollment: enrollment)
create(:hmis_disability, data_source: ds1, enrollment: enrollment)
create(:hmis_health_and_dv, data_source: ds1, enrollment: enrollment)
create(:hmis_current_living_situation, data_source: ds1, enrollment: enrollment)
create(:hmis_hud_assessment, data_source: ds1, enrollment: enrollment)
create(:hmis_employment_education, data_source: ds1, enrollment: enrollment)
create(:hmis_youth_education_status, data_source: ds1, enrollment: enrollment)

enrollment.save_not_in_progress!
end

it 'preserve shared data after destroy' do
enrollment.destroy
enrollment.destroy!
enrollment.reload

[
Expand Down Expand Up @@ -137,7 +123,7 @@
expect(enrollment.send(assoc)).to be_present, "expected #{assoc} to be present"
end

enrollment.destroy
enrollment.destroy!
enrollment.reload

[
Expand Down Expand Up @@ -527,4 +513,82 @@
expect(hh_member.current_unit).to eq(unit)
end
end

describe 'destroy hooks for outgoing CE referrals' do
before(:each) do
allow_any_instance_of(Hmis::Ce::Configuration).to receive(:enabled?).and_return(true)
end

let!(:source_enrollment) { create(:hmis_hud_enrollment, data_source: ds1, project: p1, client: c1) }
let!(:target_project) { create(:hmis_hud_project, data_source: ds1) }
let!(:target_opportunity1) { create(:hmis_ce_opportunity, data_source: ds1, project: target_project) }
let!(:target_opportunity2) { create(:hmis_ce_opportunity, data_source: ds1, project: target_project) }
let!(:target_opportunity3) { create(:hmis_ce_opportunity, data_source: ds1, project: target_project) }

context 'when enrollment is the source for active direct referrals' do
let!(:active_direct_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity1, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::DIRECT_SEND_ORIGIN, status: 'in_progress') }

it 'prevents deletion' do
expect do
source_enrollment.destroy!
end.to raise_error(ActiveRecord::DeleteRestrictionError, /Cannot delete enrollment because it has active outgoing referrals/).
and(not_change { source_enrollment.reload.date_deleted }).
and(not_change { active_direct_referral.reload.source_enrollment_id })
end
end

context 'when enrollment is the source for completed direct referrals' do
let!(:accepted_direct_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity1, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::DIRECT_SEND_ORIGIN, status: 'accepted') }
let!(:rejected_direct_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity2, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::DIRECT_SEND_ORIGIN, status: 'rejected') }

it 'destroys all direct referrals' do
expect do
source_enrollment.destroy!
end.to change { source_enrollment.reload.date_deleted }.from(nil).
and change { accepted_direct_referral.reload.deleted_at }.from(nil).
and change { rejected_direct_referral.reload.deleted_at }.from(nil)
end
end

context 'when enrollment is the source for waitlist referrals' do
let!(:active_waitlist_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity1, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::WAITLIST_ORIGIN, status: 'in_progress') }
let!(:accepted_waitlist_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity2, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::WAITLIST_ORIGIN, status: 'accepted') }
let!(:rejected_waitlist_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity3, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::WAITLIST_ORIGIN, status: 'rejected') }

it 'allows deletion even with active waitlist referrals and nullifies source_enrollment_id' do
expect do
source_enrollment.destroy!
end.to change { source_enrollment.reload.date_deleted }.from(nil).
and change { active_waitlist_referral.reload.source_enrollment_id }.from(source_enrollment.id).to(nil).
and change { accepted_waitlist_referral.reload.source_enrollment_id }.from(source_enrollment.id).to(nil).
and change { rejected_waitlist_referral.reload.source_enrollment_id }.from(source_enrollment.id).to(nil)
end
end

context 'when enrollment has mixed referral types' do
let!(:active_direct_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity1, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::DIRECT_SEND_ORIGIN, status: 'in_progress') }
let!(:completed_direct_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity2, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::DIRECT_SEND_ORIGIN, status: 'accepted') }
let!(:waitlist_referral) { create(:hmis_ce_referral, data_source: ds1, opportunity: target_opportunity3, source_enrollment: source_enrollment, client: c1, referral_origin: Hmis::Ce::Referral::WAITLIST_ORIGIN, status: 'accepted') }

it 'prevents deletion due to active direct referral' do
expect do
source_enrollment.destroy!
end.to raise_error(ActiveRecord::DeleteRestrictionError).
and(not_change { source_enrollment.reload.date_deleted }).
and(not_change { active_direct_referral.reload.source_enrollment_id }).
and(not_change { completed_direct_referral.reload.source_enrollment_id }).
and(not_change { waitlist_referral.reload.source_enrollment_id })
end

it 'allows deletion and processes referrals correctly when active direct referral is completed' do
active_direct_referral.update!(status: 'rejected')

expect do
source_enrollment.destroy!
end.to change { source_enrollment.reload.date_deleted }.from(nil).
and change { completed_direct_referral.reload.deleted_at }.from(nil).
and change { waitlist_referral.reload.source_enrollment_id }.from(source_enrollment.id).to(nil)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@
status
sourceEnrollmentId
clientId
targetProjectId
targetProjectName
targetProjectType
customStatus {
id
key
name
}
referredBy {
id
name
}

# special case summary field that is resolved when the user has permission to view the client
clientName
Expand Down
Loading