Skip to content

Commit

Permalink
add eligibility rules for alive status verification type (#4041)
Browse files Browse the repository at this point in the history
* add eligibility rules for alive status verification type

* refactor validation logic

* rubocop fix

* add more code changes and specs

* add specs

* fix typo

* add more specs

---------

Co-authored-by: haridhar yamjala <[email protected]>
  • Loading branch information
vkghub and ymhari authored Jul 30, 2024
1 parent 7eabf4b commit 3534a6f
Show file tree
Hide file tree
Showing 8 changed files with 391 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ module DmfDetermination
class BuildCv3FamilyPayloadForDmf
include Dry::Monads[:result, :do]
include ::Operations::Transmittable::TransmittableUtils
include DmfUtils

# @return [ Cv3Family ] Job successfully completed
def call(family, transmittable_params)
Expand All @@ -22,7 +21,6 @@ def call(family, transmittable_params)

cv3_family = yield build_cv3_family
valid_cv3_family = yield validate_cv3_family(cv3_family)

yield validate_all_family_members(valid_cv3_family)
result = yield confirm_transmittable_payload(valid_cv3_family)

Expand All @@ -32,47 +30,48 @@ def call(family, transmittable_params)
private

def build_cv3_family
cv3_family = Operations::Transformers::FamilyTo::Cv3Family.new.call(@family)
return handle_dmf_failure("Unable to transform family into cv3_family: #{cv3_family.failure}", :build_cv3_family) if cv3_family.failure?
result = Operations::Transformers::FamilyTo::Cv3Family.new.call(@family)
return handle_dmf_failure("Unable to transform family into cv3_family: #{result.failure}", :build_cv3_family) if result.failure?

cv3_family
result
end

def validate_cv3_family(cv3_family)
valid_cv3_family = AcaEntities::Operations::CreateFamily.new.call(cv3_family)
return handle_dmf_failure("Invalid cv3 family: #{valid_cv3_family.failure}", :validate_cv3_family) if valid_cv3_family.failure?
result = AcaEntities::Operations::CreateFamily.new.call(cv3_family)
return handle_dmf_failure("Invalid cv3 family: #{result.failure}", :validate_cv3_family) if result.failure?

valid_cv3_family
result
end

def validate_all_family_members(aca_family)
invalid_persons = aca_family.family_members.map do |aca_member|
family_member = @family.family_members.detect { |fm| fm.hbx_id == aca_member.hbx_id }
def validate_all_family_members(family_entity)
family_members = @family.family_members
entity_subjects = family_entity.eligibility_determination.subjects

# check if member does not have an enrollment
unless member_dmf_determination_eligible_enrollments(family_member, @family)
vh_message = "Family Member with hbx_id #{aca_member.hbx_id} does not have a valid enrollment"
update_verification_type_histories(vh_message, [family_member])
next vh_message
end
members_data = family_entity.family_members.collect do |member_entity|
family_member = family_members.detect { |fm| fm.hbx_id == member_entity.hbx_id }
entity_subject = entity_subjects.collect{|_k,v| v if v[:hbx_id] == member_entity.hbx_id }.flatten.compact.first

# check if member is valid (valid_ssn, etc.)
encrypted_ssn = aca_member&.person&.person_demographics&.encrypted_ssn
# using the same Validator used by FDSH for consistency
valid_ssn = AcaEntities::Operations::EncryptedSsnValidator.new.call(encrypted_ssn)
next if valid_ssn.success?
result = Operations::Fdsh::PayloadEligibility::CheckDeterminationSubjectEligibilityRules.new.call(entity_subject, :alive_status)

vh_message = "Family Member with hbx_id #{aca_member.hbx_id} is not valid: #{valid_ssn.failure}"
update_verification_type_histories(vh_message, [family_member])
vh_message
if result.success?
{member_entity.hbx_id => {'status' => true, 'error' => :no_errors}}
else
error = result.failure
person = family_member.person
message = "Family Member is not eligible for DMF Determination due to errors: #{error}"
add_verification_history(person, "DMF_Request_Failed", message)
{member_entity.hbx_id => {'status' => false, 'error' => result.failure}}
end
end.compact

# invalid_persons.size == family_members.size indicates no family members were valid
return Success(aca_family) unless invalid_persons.size == aca_family.family_members.size
member_status = members_data.collect { |hash| hash.collect{|_k,v| v["status"]} }.flatten.compact

message = "DMF Determination not sent: no family members are eligible"
# 'false' as third param prevent updating verification histories -> have already been updated
handle_dmf_failure(message, :build_cv3_family, update_histories: false)
if member_status.all?(false)
message = "DMF Determination not sent: no family members are eligible"
handle_dmf_failure(message, :build_cv3_family, update_histories: false)
else
Success(family_entity)
end
end

def confirm_transmittable_payload(valid_cv3_family)
Expand Down Expand Up @@ -107,11 +106,15 @@ def handle_dmf_failure(message, state, update_histories: true)

def update_verification_type_histories(message, family_members = @family.family_members)
family_members.each do |member|
alive_status_verification = member&.person&.alive_status
next unless alive_status_verification
alive_status_verification.add_type_history_element(action: "DMF Determination Request Failure", modifier: "System", update_reason: message)
add_verification_history(member.person, "DMF_Request_Failed", message)
end
end

def add_verification_history(person, action, update_reason)
alive_status_verification = person.verification_types.alive_status_type.first
return unless alive_status_verification
alive_status_verification.add_type_history_element(action: action, modifier: "System", update_reason: update_reason)
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def call(payload)

@transmission = yield build_and_create_request_transmission(values)
@transaction = yield build_and_create_request_transaction(values)

yield record_verification_history
payload = yield build_cv3_family_payload
event = yield build_event(payload)
result = yield publish(event)
Expand Down Expand Up @@ -98,13 +98,20 @@ def build_event(payload)
event('events.families.verifications.dmf_determination.requested', attributes: payload)
end

def record_verification_history
@family.family_members.each do |member|
message = "DMF Determination request for Family with hbx_id #{@family.hbx_assigned_id} is submitted"
person = member.person
alive_status_type = person.verification_types.alive_status_type.first
alive_status_type.add_type_history_element(action: "DMF_Request_Submitted", modifier: "System", update_reason: message)
end

Success(true)
end

def publish(event)
hbx_id = @transaction.json_payload[:family_hash][:hbx_id]
event.publish

message = "DMF Determination request for Family with hbx_id #{hbx_id} sent successfully"
people = @family.family_members.map(&:person)
people.select(&:alive_status).each { |p| p.alive_status.add_type_history_element(action: "DMF Determination Request", modifier: "System", update_reason: message) }
message = "DMF Determination request for Family with hbx_id #{@family.hbx_assigned_id} is submitted"
update_status(message, :succeeded, { transmission: @transmission, transaction: @transaction })

Success(message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class CheckBaseEligibilityRules
ssa: [:validate_ssn],
dhs: [:validate_vlp_documents],
local_residency: [],
alive_status: [:validate_ssn],
alive_status: [:validate_ssn, :is_member_enrolled?],
income: [:validate_ssn],
esi_mec: [:validate_ssn],
non_esi_mec: [:validate_ssn],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

# This module defines operations related to FDSH payload eligibility.
module Operations
module Fdsh
module PayloadEligibility
# This class checks the eligibility rules for a determination subject within the FDSH payload.
# It extends `CheckBaseEligibilityRules` to leverage common eligibility rule checks.
class CheckDeterminationSubjectEligibilityRules < CheckBaseEligibilityRules
# Valid eligibility states for health and dental product enrollments.
VALID_ELIGIBLITY_STATES = [
'health_product_enrollment_status',
'dental_product_enrollment_status'
].freeze

private

# Validates the payload entity to ensure it is a valid Subject object and then
# calls the superclass's validate method.
#
# @param payload_entity [Subject Hash] The subject entity to validate.
# @param request_type [Symbol] The type of request being validated.
# @return [Dry::Monads::Result] Success or Failure indicating the validation result.
def validate(payload_entity, request_type)
return Failure("Invalid Subject Object #{payload_entity}") unless payload_entity.is_a?(Hash)
super(request_type)
end

# Validates the Social Security Number (SSN) of the payload entity.
#
# @param payload_entity [Subject Hash] The subject entity whose SSN is to be validated.
# @return [Dry::Monads::Result] Success or Failure indicating the validation result of the SSN.
def validate_ssn(payload_entity)
encrypted_ssn = payload_entity[:encrypted_ssn]
return Failure("No SSN for member #{payload_entity[:hbx_id]}") if encrypted_ssn.nil? || encrypted_ssn.empty?

AcaEntities::Operations::EncryptedSsnValidator.new.call(encrypted_ssn)
end

# Checks if the member is enrolled based on the eligibility states provided in the payload.
#
# @param payload_entity [Subject Hash] The subject entity to check enrollment status for.
# @return [Dry::Monads::Result] Success if the subject is enrolled in either health or dental enrollment,
# otherwise Failure.
def is_member_enrolled?(payload_entity)
states = payload_entity[:eligibility_states].collect { |k, v| v[:is_eligible] if VALID_ELIGIBLITY_STATES.include?(k.to_s) }.flatten.compact

return Failure("No states found for the given subject/member hbx_id: #{payload_entity[:hbx_id]} ") unless states.present?
return Success() if states.any?(true)

Failure("subject is not enrolled in health or dental enrollment")
end
end
end
end
end
9 changes: 7 additions & 2 deletions app/models/eligibilities/subject.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ def serializable_cv_hash
]
end.reduce(:merge)

subject_attributes = attributes.slice('first_name', 'last_name', 'encrypted_ssn', 'hbx_id',
'person_id', 'outstanding_verification_status', 'is_primary')
subject_attributes = attributes.symbolize_keys.slice(:first_name, :last_name, :encrypted_ssn, :hbx_id, :person_id, :is_primary, :outstanding_verification_status)

if subject_attributes[:encrypted_ssn].present?
encrypted_ssn = AcaEntities::Operations::Encryption::Encrypt.new.call(value: SymmetricEncryption.decrypt(subject_attributes[:encrypted_ssn])).value! # For CV3 payload
subject_attributes[:encrypted_ssn] = encrypted_ssn
end

subject_attributes[:dob] = dob
subject_attributes[:eligibility_states] = eligibility_states_hash

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,30 @@
RSpec.describe Operations::Families::Verifications::DmfDetermination::BuildCv3FamilyPayloadForDmf, dbclean: :after_each do
include Dry::Monads[:result, :do]

let(:family) { FactoryBot.create(:individual_market_family_with_spouse) }
let(:primary) { family.primary_person }
let(:primary_dob){ Date.today - 57.years }
let(:family) do
FactoryBot.create(:family, :with_primary_family_member, :person => primary)
end

let(:spouse_person) { FactoryBot.create(:person, :with_consumer_role, dob: spouse_dob, ssn: 101_011_012) }
let!(:spouse) { FactoryBot.create(:family_member, person: spouse_person, family: family) }
let(:spouse_dob) { Date.today - 55.years }
let(:product) { FactoryBot.create(:benefit_markets_products_health_products_health_product, :with_issuer_profile, metal_level_kind: :silver, benefit_market_kind: :aca_individual) }
let!(:enrollment) do
FactoryBot.create(:hbx_enrollment,
:with_enrollment_members,
family: family,
enrollment_members: enrolled_members,
household: family.active_household,
coverage_kind: :health,
effective_on: Date.today,
kind: "individual",
product: product,
rating_area_id: primary.consumer_role.rating_address.id,
consumer_role_id: family.primary_person.consumer_role.id,
aasm_state: 'coverage_selected')
end

let(:date) { DateTime.now }

let(:job) do
Expand Down Expand Up @@ -58,47 +80,34 @@
end

let(:transmittable_params) {{ job: job, transmission: transmission, transaction: transaction }}

# lambda to change member eligibility
let(:change_member_eligibility) do
lambda { |member_hbx_ids|
subjects = family.eligibility_determination.subjects
eligible_subjects = subjects.select { |sub| member_hbx_ids.include?(sub.hbx_id) }
eligible_subjects.each do |subject|
key = 'health_product_enrollment_status'
state = subject.eligibility_states.where(eligibility_item_key: key).first
state.update(is_eligible: true)
end
}
end

let(:dependent) { family.dependents.last.person }

before do
BenefitMarkets::Products::ProductRateCache.initialize_rate_cache!
allow(EnrollRegistry[:alive_status].feature).to receive(:is_enabled).and_return(true)
# need to run this operation to accurately handle cv3 family
primary.build_demographics_group
spouse_person.build_demographics_group
Operations::Eligibilities::BuildFamilyDetermination.new.call({ effective_date: Date.today, family: family })
end

context "success" do
let(:primary) { FactoryBot.create(:person, :with_consumer_role, dob: primary_dob, ssn: 101_011_011) }

context 'with all valid members with an enrollment' do
it "should pass" do
# everyone subject is made eligible for enrollment
all_member_ids = family.family_members.map(&:hbx_id)
change_member_eligibility[all_member_ids]
let(:spouse_person) { FactoryBot.create(:person, :with_consumer_role, dob: spouse_dob, ssn: 101_011_012) }
let(:enrolled_members) { family.family_members }

it "should pass" do
result = described_class.new.call(family, transmittable_params)
expect(result).to be_success
end
end

context 'with some non-enrolled members' do
let(:check_eligibility_rules) { double(Operations::Fdsh::PayloadEligibility::CheckPersonEligibilityRules) }
let(:spouse_person) { FactoryBot.create(:person, :with_consumer_role, dob: spouse_dob, ssn: 101_011_012) }
let(:enrolled_members) { [family.family_members.first] }

before do
# dependent subject is not made eligible for enrollment
change_member_eligibility[primary.hbx_id]

@result = described_class.new.call(family, transmittable_params)
end

Expand All @@ -110,18 +119,16 @@
dependent.reload
element = dependent.alive_status.type_history_elements.last

expect(element.action).to eq 'DMF Determination Request Failure'
expect(element.update_reason).to eq "Family Member with hbx_id #{dependent.hbx_id} does not have a valid enrollment"
expect(element.action).to eq 'DMF_Request_Failed'
expect(element.update_reason).to eq "Family Member is not eligible for DMF Determination due to errors: [\"No states found for the given subject/member hbx_id: #{dependent.hbx_id} \"]"
end
end

context 'with some members with invalid ssns' do
before do
# everyone subject is made eligible for enrollment
all_member_ids = family.family_members.map(&:hbx_id)
change_member_eligibility[all_member_ids]
let(:spouse_person) { FactoryBot.create(:person, :with_consumer_role, dob: spouse_dob, ssn: nil) }
let(:enrolled_members) { family.family_members }

dependent.update(ssn: '999999999')
before do
@result = described_class.new.call(family, transmittable_params)
end

Expand All @@ -133,19 +140,17 @@
dependent.reload
element = dependent.alive_status.type_history_elements.last

expect(element.action).to eq 'DMF Determination Request Failure'
expect(element.update_reason).to eq "Family Member with hbx_id #{dependent.hbx_id} is not valid: Invalid SSN"
expect(element.action).to eq 'DMF_Request_Failed'
expect(element.update_reason).to eq "Family Member is not eligible for DMF Determination due to errors: [\"No SSN for member #{dependent.hbx_id}\"]"
end
end

context 'parsing cv3_family after being published' do
let(:enrolled_members) { family.family_members }

it "should be able to be parsed from JSON and validate with AcaEntities::Operations::CreateFamily" do
# everyone subject is made eligible for enrollment
all_member_ids = family.family_members.map(&:hbx_id)
change_member_eligibility[all_member_ids]
described_class.new.call(family, transmittable_params)
transaction.reload

# we will convert this to json and then parse with JSON to simulate FDSH handling the event
payload = transaction.json_payload[:family_hash]
json_payload = payload.to_json
Expand All @@ -158,6 +163,8 @@
end

context "failure" do
let(:primary) { FactoryBot.create(:person, :with_consumer_role, dob: primary_dob, ssn: 101_011_011) }
let(:enrolled_members) { [family.family_members.first] }
let(:fake_cv3_transformer) { double(Operations::Transformers::FamilyTo::Cv3Family) }

context 'cv3 transformation failure' do
Expand Down Expand Up @@ -229,6 +236,8 @@
end

context 'with all members ineligible' do
let(:enrolled_members) { [] }

before do
@result = described_class.new.call(family, transmittable_params)
end
Expand All @@ -241,7 +250,7 @@
it 'should add a history element to all alive_status verifications' do
alive_status_elements = [primary.alive_status, dependent.alive_status].map(&:type_history_elements)

expect(alive_status_elements.all? { |elements| elements.last.update_reason.include?('does not have a valid enrollment') }).to be_truthy
expect(alive_status_elements.all? { |elements| elements.last.update_reason.match?("No states found for the given subject/member hbx_id") }).to be_truthy
end

it "should update the transmission" do
Expand Down
Loading

0 comments on commit 3534a6f

Please sign in to comment.