From cf74bfc60681a2f24066fcb6be8bc1cc33bfada5 Mon Sep 17 00:00:00 2001 From: Charlie Parker Date: Mon, 1 Dec 2025 16:01:15 -0500 Subject: [PATCH 1/5] handle applicant ssn removal for faa flow (#5927) * unset encrypted ssn to circumvent sparse index error * add people update spec * spec fixes --------- Co-authored-by: Jacob Kagon <69021620+jacobkagon@users.noreply.github.com> --- .../people/create_or_update.rb | 7 +- .../people/create_or_update_spec.rb | 422 ++++++++++++++++++ 2 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 spec/domain/operations/financial_assistance/on_determination/people/create_or_update_spec.rb diff --git a/app/domain/operations/financial_assistance/on_determination/people/create_or_update.rb b/app/domain/operations/financial_assistance/on_determination/people/create_or_update.rb index a57cfd416f6..29c84aa4dc9 100644 --- a/app/domain/operations/financial_assistance/on_determination/people/create_or_update.rb +++ b/app/domain/operations/financial_assistance/on_determination/people/create_or_update.rb @@ -97,7 +97,6 @@ def update_person(person, applicant) middle_name: applicant.middle_name, last_name: applicant.last_name, name_sfx: applicant.name_sfx, - encrypted_ssn: applicant.encrypted_ssn, no_ssn: applicant.no_ssn, gender: applicant.gender, dob: applicant.dob, @@ -116,6 +115,12 @@ def update_person(person, applicant) is_temporarily_out_of_state: applicant.is_temporarily_out_of_state ) + if applicant.encrypted_ssn.blank? + person.unset(:encrypted_ssn) + else + person.encrypted_ssn = applicant.encrypted_ssn + end + Success(person) end diff --git a/spec/domain/operations/financial_assistance/on_determination/people/create_or_update_spec.rb b/spec/domain/operations/financial_assistance/on_determination/people/create_or_update_spec.rb new file mode 100644 index 00000000000..14806ba997b --- /dev/null +++ b/spec/domain/operations/financial_assistance/on_determination/people/create_or_update_spec.rb @@ -0,0 +1,422 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Operations::FinancialAssistance::OnDetermination::People::CreateOrUpdate, type: :model, dbclean: :after_each do + let(:family) { FactoryBot.create(:family, :with_primary_family_member, person: primary_person) } + let(:application) { FactoryBot.create(:financial_assistance_application, family_id: family.id) } + let(:determination) { FactoryBot.create(:financial_assistance_eligibility_determination, application: application) } + + let(:primary_person) { FactoryBot.create(:person, :with_consumer_role) } + let(:primary_family_member_id) { family.primary_applicant.id } + + let(:primary_address) do + FactoryBot.build( + :financial_assistance_address, + kind: 'home', + address_1: '123 Test St', + city: 'Washington', + state: 'DC', + zip: '20001' + ) + end + + let(:primary_email) { FactoryBot.build(:financial_assistance_email, kind: 'home', address: 'test@example.com') } + let(:primary_phone) { FactoryBot.build(:financial_assistance_phone, kind: 'home', area_code: '202', number: '1234567') } + + let(:applicant) do + FactoryBot.create( + :financial_assistance_applicant, + is_primary_applicant: true, + family_member_id: primary_family_member_id, + person_hbx_id: primary_person.hbx_id, + eligibility_determination_id: determination.id, + addresses: [primary_address], + emails: [primary_email], + phones: [primary_phone], + application: application, + first_name: 'John', + last_name: 'Doe', + gender: 'male', + dob: Date.new(1980, 1, 1), + encrypted_ssn: encrypted_ssn, + no_ssn: no_ssn + ) + end + + let(:encrypted_ssn) { SymmetricEncryption.encrypt('123456789') } + let(:no_ssn) { '0' } + + before :each do + allow(EnrollRegistry).to receive(:feature_enabled?).with(:qhp_application).and_return(true) + allow(EnrollRegistry[:alive_status].feature).to receive(:is_enabled).and_return(true) + end + + describe '#call' do + context 'when applicant has an SSN' do + let(:encrypted_ssn) { SymmetricEncryption.encrypt('123456789') } + let(:no_ssn) { '0' } + + before :each do + @result = subject.call(applicant: applicant) + end + + it 'returns a success result' do + expect(@result.success?).to be_truthy + end + + it 'assigns encrypted_ssn to the person' do + person = @result.success + expect(person.encrypted_ssn).to eq(applicant.encrypted_ssn) + expect(person.encrypted_ssn).to be_present + end + + it 'assigns the correct SSN value' do + person = @result.success + expect(person.ssn).to eq('123456789') + end + + it 'sets no_ssn flag to false when SSN is present' do + person = @result.success + expect(person.no_ssn).to eq('0') + end + + it 'creates a consumer role with correct attributes' do + person = @result.success + expect(person.consumer_role).to be_present + expect(person.consumer_role.is_applicant).to eq(applicant.is_primary_applicant) + expect(person.consumer_role.contact_method).to eq(applicant.contact_method) + expect(person.consumer_role.is_applying_coverage).to eq(applicant.is_applying_coverage) + end + + it 'creates addresses from applicant information' do + person = @result.success + expect(person.addresses.size).to eq(1) + address = person.addresses.first + expect(address.kind).to eq('home') + expect(address.address_1).to eq('123 Test St') + expect(address.city).to eq('Washington') + expect(address.state).to eq('DC') + expect(address.zip).to eq('20001') + end + + it 'creates emails from applicant information' do + person = @result.success + expect(person.emails.size).to eq(1) + email = person.emails.first + expect(email.kind).to eq('home') + expect(email.address).to eq('test@example.com') + end + + it 'creates phones from applicant information' do + person = @result.success + expect(person.phones.size).to eq(1) + phone = person.phones.first + expect(phone.kind).to eq('home') + expect(phone.area_code).to eq('202') + expect(phone.number).to eq('1234567') + end + + it 'creates a demographics group' do + person = @result.success + expect(person.demographics_group).to be_present + end + + it 'persists the person to the database' do + person = @result.success + expect(person.persisted?).to be_truthy + expect(person.id).to be_present + end + + context 'when person matching finds existing person with same SSN' do + # Skip the main before block for this context to avoid SSN conflicts + let!(:existing_person) do + Person.destroy_all # Clear any existing people first + FactoryBot.create(:person, + first_name: 'John', + last_name: 'Doe', + dob: Date.new(1980, 1, 1), + encrypted_ssn: SymmetricEncryption.encrypt('123456789')) + end + + # Create a new applicant without family_member_id to force SSN matching + let(:matching_applicant) do + FactoryBot.create( + :financial_assistance_applicant, + is_primary_applicant: false, + family_member_id: nil, # Key: no family member ID to force SSN matching + person_hbx_id: nil, + eligibility_determination_id: determination.id, + addresses: [primary_address], + emails: [primary_email], + phones: [primary_phone], + application: application, + first_name: 'John', + last_name: 'Doe', + gender: 'male', + dob: Date.new(1980, 1, 1), + encrypted_ssn: SymmetricEncryption.encrypt('123456789'), + no_ssn: '0' + ) + end + + before do + # Don't use @result from the main context, run the operation fresh + @result = subject.call(applicant: matching_applicant) + end + + it 'updates the existing person instead of creating a new one' do + person = @result.success + expect(person.id).to eq(existing_person.id) + end + + it 'updates person attributes from applicant' do + person = @result.success + expect(person.gender).to eq(matching_applicant.gender) + end + end + end + + context 'when applicant does not have an SSN' do + let(:encrypted_ssn) { nil } + let(:no_ssn) { '1' } + + before :each do + applicant.encrypted_ssn = nil + applicant.no_ssn = '1' + @result = subject.call(applicant: applicant) + end + + it 'returns a success result' do + expect(@result.success?).to be_truthy + end + + it 'does not assign encrypted_ssn to the person' do + person = @result.success + expect(person.encrypted_ssn).to be_blank + expect(person.encrypted_ssn).to be_nil + end + + it 'assigns no_ssn flag to the person' do + person = @result.success + expect(person.no_ssn).to eq('1') + end + + it 'has blank SSN value' do + person = @result.success + expect(person.ssn).to be_blank + expect(person.ssn).to be_nil + end + + it 'handles SSN validation correctly for no_ssn cases' do + person = @result.success + expect(person.no_ssn).to eq('1') + expect(person.ssn).to be_nil + expect(person.encrypted_ssn).to be_nil + end + + it 'still creates a consumer role' do + person = @result.success + expect(person.consumer_role).to be_present + end + + it 'still creates contact information' do + person = @result.success + expect(person.addresses.size).to eq(1) + expect(person.emails.size).to eq(1) + expect(person.phones.size).to eq(1) + end + + context 'when person matching without SSN' do + let!(:existing_person) do + FactoryBot.create(:person, + first_name: 'John', + last_name: 'Doe', + dob: Date.new(1980, 1, 1), + encrypted_ssn: nil, + no_ssn: '1') + end + + it 'performs matching using name and DOB only' do + person = @result.success + expect(person.first_name).to eq('John') + expect(person.last_name).to eq('Doe') + expect(person.dob).to eq(Date.new(1980, 1, 1)) + end + end + end + + context 'when applicant has family_member_id' do + let(:existing_person) { FactoryBot.create(:person, :with_consumer_role) } + let(:family_member) { FactoryBot.create(:family_member, family: family, person: existing_person) } + + before :each do + applicant.family_member_id = family_member.id + applicant.person_hbx_id = existing_person.hbx_id + @result = subject.call(applicant: applicant) + end + + it 'finds the existing person through family member' do + person = @result.success + expect(person.id).to eq(existing_person.id) + end + + it 'updates the existing person with applicant information' do + person = @result.success + expect(person.first_name).to eq(applicant.first_name) + expect(person.last_name).to eq(applicant.last_name) + expect(person.gender).to eq(applicant.gender) + end + end + + context 'when applicant has no VLP document information' do + before :each do + applicant.vlp_subject = nil + applicant.alien_number = nil + applicant.i94_number = nil + @result = subject.call(applicant: applicant) + end + + it 'does not create VLP documents' do + person = @result.success + expect(person.consumer_role.vlp_documents.size).to eq(0) + end + + it 'still creates lawful presence determination with citizen status' do + person = @result.success + expect(person.consumer_role.lawful_presence_determination).to be_present + expect(person.consumer_role.lawful_presence_determination.citizen_status).to eq(applicant.citizen_status) + end + end + + context 'when updating existing person with different contact information' do + let(:existing_person) do + person = FactoryBot.create(:person, + first_name: 'John', + last_name: 'Doe', + dob: Date.new(1980, 1, 1), + encrypted_ssn: SymmetricEncryption.encrypt('123456789')) + person.addresses.build(kind: 'home', address_1: 'Old Address', city: 'Old City', state: 'DC', zip: '20002') + person.emails.build(kind: 'home', address: 'old@example.com') + person.phones.build(kind: 'home', area_code: '301', number: '9876543') + person.save! + person + end + + before :each do + # Force matching to find the existing person + operation_instance = described_class.new + allow(described_class).to receive(:new).and_return(operation_instance) + allow(operation_instance).to receive(:find_existing_person).and_return(existing_person) + @result = subject.call(applicant: applicant) + end + + it 'replaces old addresses with new ones' do + person = @result.success + expect(person.addresses.size).to eq(1) + address = person.addresses.first + expect(address.address_1).to eq('123 Test St') + expect(address.city).to eq('Washington') + end + + it 'replaces old emails with new ones' do + person = @result.success + expect(person.emails.size).to eq(1) + email = person.emails.first + expect(email.address).to eq('test@example.com') + end + + it 'replaces old phones with new ones' do + person = @result.success + expect(person.phones.size).to eq(1) + phone = person.phones.first + expect(phone.area_code).to eq('202') + expect(phone.number).to eq('1234567') + end + end + + context 'with ethnicity and tribal information' do + let(:applicant_with_tribal_info) do + FactoryBot.create( + :financial_assistance_applicant, + is_primary_applicant: false, + eligibility_determination_id: determination.id, + addresses: [primary_address], + emails: [primary_email], + phones: [primary_phone], + application: application, + first_name: 'Tribal', + last_name: 'Member', + gender: 'female', + dob: Date.new(1990, 3, 15), + encrypted_ssn: SymmetricEncryption.encrypt('456789012'), + ethnicity: ['American Indian or Alaska Native', 'Hispanic or Latino'], + race: 'American Indian or Alaska Native', + indian_tribe_member: true, + tribal_id: 'T12345', + tribal_state: 'AK', + tribal_name: 'Test Tribe' + ) + end + + before :each do + @result = subject.call(applicant: applicant_with_tribal_info) + end + + it 'assigns ethnicity information' do + person = @result.success + expect(person.ethnicity).to contain_exactly('American Indian or Alaska Native', 'Hispanic or Latino') + end + + it 'assigns tribal information' do + person = @result.success + expect(person.race).to eq('American Indian or Alaska Native') + expect(person.tribal_id).to eq('T12345') + expect(person.tribal_state).to eq('AK') + expect(person.tribal_name).to eq('Test Tribe') + end + end + end + + describe 'private methods' do + describe '#find_existing_person' do + let(:operation) { described_class.new } + + context 'when applicant has family_member_id' do + let(:existing_person) { FactoryBot.create(:person) } + let(:family_member) { FactoryBot.create(:family_member, family: family, person: existing_person) } + + it 'returns the person through family member' do + applicant.family_member_id = family_member.id + result = operation.send(:find_existing_person, applicant) + expect(result).to eq(existing_person) + end + end + + context 'when applicant has no family_member_id' do + it 'uses matching criteria to find person' do + # Test the matching logic + result = operation.send(:find_existing_person, applicant) + # Since there's no exact match, it should return nil or use the matching service + expect(result).to be_a(Person).or be_nil + end + end + end + + describe '#fetch_ethnicity' do + let(:operation) { described_class.new } + + it 'filters out blank ethnicity values' do + applicant.ethnicity = ['Hispanic or Latino', '', nil, 'American Indian or Alaska Native', ''] + result = operation.send(:fetch_ethnicity, applicant) + expect(result).to contain_exactly('Hispanic or Latino', 'American Indian or Alaska Native') + end + + it 'returns empty array for non-array ethnicity' do + applicant.ethnicity = nil + result = operation.send(:fetch_ethnicity, applicant) + expect(result).to eq([]) + end + end + end +end \ No newline at end of file From aab8dcb249d9bdd619209a900f887d818b66d489 Mon Sep 17 00:00:00 2001 From: vishal kalletla Date: Mon, 8 Dec 2025 09:38:05 -0500 Subject: [PATCH 2/5] rebuild family determination on failed request (#5940) * rebuild family determination on failed request * fix spec * fix more specs * spec fix --- .../income_evidence/request_verification.rb | 25 +++-- .../shared/non_esi_evidence_request.rb | 15 ++- .../request_verification_spec.rb | 46 ++++++--- .../request_verification_spec.rb | 95 +++++++++++-------- .../request_verification_spec.rb | 28 +++--- .../shared/non_esi_evidence_request_spec.rb | 4 +- 6 files changed, 120 insertions(+), 93 deletions(-) diff --git a/components/financial_assistance/app/domain/financial_assistance/operations/applications/rrv/income_evidence/request_verification.rb b/components/financial_assistance/app/domain/financial_assistance/operations/applications/rrv/income_evidence/request_verification.rb index 17eabe2c5a1..af6503befff 100644 --- a/components/financial_assistance/app/domain/financial_assistance/operations/applications/rrv/income_evidence/request_verification.rb +++ b/components/financial_assistance/app/domain/financial_assistance/operations/applications/rrv/income_evidence/request_verification.rb @@ -25,13 +25,13 @@ def call(params) event = yield build_event(cv3_application) publish(event) - Success("Successfully published payload for rrv ifsv and created history event | family_eligibility_determination: #{determination_result}") + Success("RRV INCOME: Successfully published payload for rrv ifsv and created history event | family_eligibility_determination: #{determination_result}") end private def validate(params) - return Failure('application_hbx_id is missing') unless params[:application_hbx_id].present? + return Failure('RRV INCOME: application_hbx_id is missing') unless params[:application_hbx_id].present? Success(params[:application_hbx_id]) end @@ -41,8 +41,7 @@ def fetch_application(application_hbx_id) if application.present? Success(application) else - rrv_logger.info("No application found with hbx_id #{application_hbx_id}") - Failure("No application found with hbx_id #{application_hbx_id}") + Failure("RRV INCOME: No application found with hbx_id #{application_hbx_id}") end end @@ -50,8 +49,7 @@ def is_application_valid?(application) if application.valid? Success(true) else - rrv_logger.error("Application with hbx_id #{application.hbx_id} is invalid: #{application.errors.full_messages.join(', ')}") - Failure("Application with hbx_id #{application.hbx_id} is invalid") + Failure("RRV INCOME: Application with hbx_id #{application.hbx_id} is invalid") end end @@ -68,18 +66,21 @@ def transform_and_validate_application(application) if result.any?(Failure) errors = result.select { |r| r.is_a?(Failure) }.map(&:failure) record_application_failure(application, errors) + rrv_logger.error("RRV INCOME: Failed to publish: Applicants validation failed with hbx_id #{application.hbx_id} due to #{errors}") + update_family_determination(application) return Failure(errors) else application.save! end else record_application_failure(application, payload_entity.failure.messages) + rrv_logger.error("RRV INCOME: Failed to publish: Application validation failed with hbx_id #{application.hbx_id} due to #{payload_entity.failure.messages}") + update_family_determination(application) end payload_entity rescue StandardError => e - rrv_logger.error("Failed to transform application with hbx_id #{application.hbx_id} due to #{e.inspect}") - Failure("Failed to transform application with hbx_id #{application.hbx_id} due to #{e.inspect}") + Failure("RRV INCOME: Failed to transform application with hbx_id #{application.hbx_id} due to #{e.inspect}") end def validate_applicants(payload_entity) @@ -114,11 +115,9 @@ def record_histories(application, action, update_reason, update_by) end def update_family_determination(application) - family = application.family - unless family.present? - rrv_logger.error("RRV INCOME: Family not found for application hbx_id: #{application.hbx_id}") - return Failure("RRV INCOME: Family not found for application hbx_id: #{application.hbx_id}") - end + family_id = application.family_id + family = Family.where(id: family_id).first + return Failure("RRV INCOME: Family not found for application hbx_id: #{application.hbx_id}") unless family.present? if family.latest_application_gid == application.to_global_id&.uri&.to_s ::Operations::Eligibilities::BuildFamilyDetermination.new.call({family: family}) diff --git a/components/financial_assistance/app/domain/financial_assistance/operations/applications/shared/non_esi_evidence_request.rb b/components/financial_assistance/app/domain/financial_assistance/operations/applications/shared/non_esi_evidence_request.rb index 322c4a17538..c6c5ef67063 100644 --- a/components/financial_assistance/app/domain/financial_assistance/operations/applications/shared/non_esi_evidence_request.rb +++ b/components/financial_assistance/app/domain/financial_assistance/operations/applications/shared/non_esi_evidence_request.rb @@ -39,7 +39,6 @@ def fetch_application(params) if application.present? application.valid? ? Success(application) : Failure("Invalid application: #{params[:application_hbx_id]}") else - logger.error("No application found with hbx_id #{params[:application_hbx_id]}") Failure("No application found with hbx_id #{params[:application_hbx_id]}") end end @@ -52,17 +51,19 @@ def transform_and_validate_application(application) move_applicant_eligibility_state(application) save_application(application) return payload_entity if all_applicants_valid.any?(&:last) + update_family_determination(application) return Failure("Failed to transform application with hbx_id #{application.hbx_id} due to all applicants are invalid") elsif payload_entity.failure? - move_applicant_eligibility_state(application) record_application_failure(application, payload_entity.failure.messages) + move_applicant_eligibility_state(application) save_application(application) + update_family_determination(application) + return Failure("Failed at validation for application with hbx_id #{application.hbx_id} due to #{payload_entity.failure.messages}") end payload_entity rescue StandardError => e - logger.error("#{process_name} process failed to publish event for the application with hbx_id #{application.hbx_id} due to #{e.inspect}") - Failure("#{process_name} process failed to publish event for the application with hbx_id #{application.hbx_id} due to #{e.inspect}") + Failure("Failed to publish event for the application with hbx_id #{application.hbx_id} due to #{e.inspect}") end def move_applicant_eligibility_state(application) @@ -157,17 +158,13 @@ def save_application(application) Success(application) else error_msg = "Failed to save application: #{application.errors.full_messages.join(', ')}" - logger.error(error_msg) Failure(error_msg) end end def update_family_determination(application) family = application.family - unless family.present? - logger.error("#{process_name} Non ESI: Family not found for application hbx_id: #{application.hbx_id}") - return Failure("#{process_name} Non ESI: Family not found for application hbx_id: #{application.hbx_id}") - end + return Failure("#{process_name} Non ESI: Family not found for application hbx_id: #{application.hbx_id}") unless family.present? if family.latest_application_gid == application.to_global_id&.uri&.to_s ::Operations::Eligibilities::BuildFamilyDetermination.new.call({family: family}) diff --git a/components/financial_assistance/spec/domain/financial_assistance/operations/applications/pvc/non_esi_evidence/request_verification_spec.rb b/components/financial_assistance/spec/domain/financial_assistance/operations/applications/pvc/non_esi_evidence/request_verification_spec.rb index 4daad5b8eca..de3ec769ab6 100644 --- a/components/financial_assistance/spec/domain/financial_assistance/operations/applications/pvc/non_esi_evidence/request_verification_spec.rb +++ b/components/financial_assistance/spec/domain/financial_assistance/operations/applications/pvc/non_esi_evidence/request_verification_spec.rb @@ -9,6 +9,29 @@ let(:person2) { FactoryBot.create(:person, :with_consumer_role, :with_active_consumer_role) } let(:person3) { FactoryBot.create(:person, :with_consumer_role, :with_active_consumer_role) } let(:family) { FactoryBot.create(:family, :with_primary_family_member, person: person) } + let!(:system_date) { Date.today } + let!(:application2) do + result = FactoryBot.create(:financial_assistance_application, hbx_id: '300000126', aasm_state: "determined", family_id: family.id, submitted_at: DateTime.new(system_date.year, system_date.month, system_date.day) - 30.minutes) + member = FactoryBot.create(:financial_assistance_applicant, + eligibility_determination_id: nil, + person_hbx_id: person.hbx_id, + is_primary_applicant: true, + first_name: 'esi', + last_name: 'evidence', + ssn: "123456789", + dob: Date.new(1994,11,17), + family_member_id: family.primary_family_member.id, + application: result) + member.build_aptc_eligibilities_evidences + member.build_ivl_eligibility_with_evidences + member.save! + family.assign_latest_application_gid + family.save! + ::Operations::Eligibilities::BuildFamilyDetermination.new.call({family: family}) + + result + end + let(:application) do FactoryBot.create( :financial_assistance_application, @@ -16,7 +39,7 @@ aasm_state: 'determined', assistance_year: TimeKeeper.date_of_record.year, effective_date: TimeKeeper.date_of_record.beginning_of_year, - created_at: Date.new(2021, 10, 1) + submitted_at: DateTime.new(system_date.year, system_date.month, system_date.day) ) end @@ -122,6 +145,7 @@ end before do + allow(EnrollRegistry).to receive(:feature_enabled?).with(:qhp_application).and_return(true) allow(FinancialAssistanceRegistry).to receive(:feature_enabled?).with(:full_medicaid_determination_step).and_return(false) allow(FinancialAssistanceRegistry).to receive(:feature_enabled?).with(:indian_alaskan_tribe_details).and_return(false) allow(FinancialAssistanceRegistry).to receive(:feature_enabled?).with(:non_esi_mec_determination).and_return(true) @@ -144,6 +168,7 @@ applicant.build_aptc_eligibilities_evidences end application.save! + family.update_attributes(latest_application_gid: application.to_global_id.uri.to_s) end it 'should return success if application hbx_id is passed' do @@ -338,14 +363,9 @@ it 'handles the exception and returns failure' do result = subject.call({ application_hbx_id: application.hbx_id }) expect(result).to be_failure - expect(result.failure).to include('PVC process failed to publish event') + expect(result.failure).to match(/Failed to publish event for the application/) expect(result.failure).to include('Unexpected error') end - - it 'logs the error' do - expect_any_instance_of(Logger).to receive(:error).with(/PVC process failed to publish event/) - subject.call({ application_hbx_id: application.hbx_id }) - end end end end @@ -447,11 +467,6 @@ expect(result).to be_failure expect(result.failure).to include('Failed to save application: Validation failed: Field is required') end - - it 'logs the error' do - expect(subject.send(:pvc_logger)).to receive(:error).with(/Failed to save application/) - subject.send(:save_application, test_application) - end end end end @@ -579,6 +594,11 @@ # Make one applicant valid and one invalid applicant.update_attributes!(ssn: "123456789") applicant2.update_attributes!(ssn: nil) # Invalid + application.applicants.each do |applicant| + allow(applicant).to receive(:is_applying_coverage).and_return(true) + applicant.build_aptc_eligibilities_evidences + end + application.save! end it 'handles mixed scenarios correctly' do @@ -613,7 +633,7 @@ result = subject.call({ application_hbx_id: application.hbx_id }) expect(result).to be_failure - expect(result.failure).to include('PVC process failed to publish event') + expect(result.failure).to match(/Failed to publish event for the application with hbx_id/) expect(result.failure).to include('Event building failed') # Since the error occurs during transform_and_validate_application (before evidence creation), diff --git a/components/financial_assistance/spec/domain/financial_assistance/operations/applications/rrv/income_evidence/request_verification_spec.rb b/components/financial_assistance/spec/domain/financial_assistance/operations/applications/rrv/income_evidence/request_verification_spec.rb index 855a765b9fa..90fbbc27a8f 100644 --- a/components/financial_assistance/spec/domain/financial_assistance/operations/applications/rrv/income_evidence/request_verification_spec.rb +++ b/components/financial_assistance/spec/domain/financial_assistance/operations/applications/rrv/income_evidence/request_verification_spec.rb @@ -6,8 +6,14 @@ include Dry::Monads[:do, :result] let(:person_1) { FactoryBot.create(:person, :with_ssn, :with_consumer_role, :with_active_consumer_role) } - let(:person_2) { FactoryBot.create(:person, :with_ssn, :with_consumer_role, :with_active_consumer_role) } + let(:person_2) do + per = FactoryBot.create(:person, :with_ssn, :with_consumer_role, :with_active_consumer_role) + person_1.ensure_relationship_with(per, 'spouse') + person_1.save! + per + end let(:family) { FactoryBot.create(:family, :with_primary_family_member, person: person_1)} + let!(:family_member_2) { FactoryBot.create(:family_member, family: family, person: person_2) } let!(:system_date) { Date.today } let!(:application2) do result = FactoryBot.create(:financial_assistance_application, hbx_id: '300000126', aasm_state: "determined", family_id: family.id, submitted_at: DateTime.new(system_date.year, system_date.month, system_date.day) - 30.minutes) @@ -15,10 +21,10 @@ eligibility_determination_id: nil, person_hbx_id: person_1.hbx_id, is_primary_applicant: true, - first_name: 'esi', - last_name: 'evidence', - ssn: "889984400", - dob: Date.new(1994,11,17), + first_name: person_1.first_name, + last_name: person_1.last_name, + encrypted_ssn: person_1.encrypted_ssn, + dob: person_1.dob, family_member_id: family.primary_family_member.id, application: result) member.build_aptc_eligibilities_evidences @@ -38,34 +44,43 @@ let(:eligibility_determination) { FactoryBot.create(:financial_assistance_eligibility_determination, application: application) } let(:applicant_1) do - FactoryBot.build(:financial_assistance_applicant, - :with_student_information, - :with_home_address, - application: application, - is_primary_applicant: true, - ssn: '889984400', - dob: Date.new(1994,11,17), - first_name: person_1.first_name, - last_name: person_1.last_name, - gender: person_1.gender, - person_hbx_id: person_1.hbx_id, - eligibility_determination_id: eligibility_determination.id) + member = FactoryBot.build(:financial_assistance_applicant, + :with_student_information, + :with_home_address, + application: application, + is_primary_applicant: true, + encrypted_ssn: person_1.encrypted_ssn, + dob: person_1.dob, + first_name: person_1.first_name, + last_name: person_1.last_name, + gender: person_1.gender, + person_hbx_id: person_1.hbx_id, + eligibility_determination_id: eligibility_determination.id) + + member.build_aptc_eligibilities_evidences + member.build_ivl_eligibility_with_evidences + member.save! + member end let(:applicant_2) do - FactoryBot.build(:financial_assistance_applicant, - :with_student_information, - :with_home_address, - :with_income_evidence, - application: application, - is_primary_applicant: false, - ssn: '889984401', - dob: Date.new(1996,11,17), - first_name: person_2.first_name, - last_name: person_2.last_name, - gender: person_2.gender, - person_hbx_id: person_2.hbx_id, - eligibility_determination_id: eligibility_determination.id) + member = FactoryBot.build(:financial_assistance_applicant, + :with_student_information, + :with_home_address, + :with_income_evidence, + application: application, + is_primary_applicant: false, + encrypted_ssn: person_2.encrypted_ssn, + dob: person_2.dob, + first_name: person_2.first_name, + last_name: person_2.last_name, + gender: person_2.gender, + person_hbx_id: person_2.hbx_id, + eligibility_determination_id: eligibility_determination.id) + member.build_aptc_eligibilities_evidences + member.build_ivl_eligibility_with_evidences + member.save! + member end let(:create_home_address) do @@ -109,29 +124,20 @@ allow(EnrollRegistry).to receive(:feature_enabled?).with(:qhp_application).and_return(true) create_home_address update_benchmark_premiums - application.applicants.each do |applicant| - aptc_csr_eligibility = FactoryBot.create(:aptc_csr_eligibility, eligible: applicant, current_state: 'pending') - old_state = FactoryBot.build(:v3_state_history, created_at: 2.days.ago) - new_state = FactoryBot.build(:v3_state_history, created_at: 1.day.ago) - aptc_csr_eligibility.state_histories << old_state - aptc_csr_eligibility.state_histories << new_state - aptc_csr_eligibility.save! - FactoryBot.create(:income_evidence, eligibility: aptc_csr_eligibility, _type: 'FinancialAssistance::Evidences::IncomeEvidence', key: :income_evidence, current_state: 'pending') - end - allow(subject).to receive(:build_event).and_return(event) allow(subject).to receive(:publish).and_return(Success("Event published successfully")) allow(HbxProfile).to receive(:current_hbx).and_return hbx_profile allow(hbx_profile).to receive(:benefit_sponsorship).and_return benefit_sponsorship allow(benefit_sponsorship).to receive(:current_benefit_period).and_return(benefit_coverage_period) - family.update_attributes(latest_application_gid: application.to_global_id.uri.to_s) end let(:payload_entity) { ::Operations::Fdsh::BuildAndValidateApplicationPayload.new.call(application).value! } context 'with valid application' do before do - allow(EnrollRegistry).to receive(:feature_enabled?).with(:qhp_application).and_return(true) + family.remove_instance_variable(:@fetch_latest_determined_application) + family.assign_latest_application_gid + family.save! @result = subject.call({application_hbx_id: application.hbx_id}) application.reload end @@ -170,12 +176,17 @@ before do application.applicants.last.unset(:encrypted_ssn) application.save! + family.remove_instance_variable(:@fetch_latest_determined_application) + family.assign_latest_application_gid + family.save! @result = subject.call({ application_hbx_id: application.hbx_id }) application.reload end it 'should return failure' do expect(@result).to be_failure + family.reload + expect(family.eligibility_determination.application_gid).to eq application.to_global_id.uri.to_s end it 'should record failure for valid applicant1' do diff --git a/components/financial_assistance/spec/domain/financial_assistance/operations/applications/rrv/non_esi_evidence/request_verification_spec.rb b/components/financial_assistance/spec/domain/financial_assistance/operations/applications/rrv/non_esi_evidence/request_verification_spec.rb index 6f00d94afc1..47fe189de5f 100644 --- a/components/financial_assistance/spec/domain/financial_assistance/operations/applications/rrv/non_esi_evidence/request_verification_spec.rb +++ b/components/financial_assistance/spec/domain/financial_assistance/operations/applications/rrv/non_esi_evidence/request_verification_spec.rb @@ -40,7 +40,7 @@ aasm_state: 'determined', assistance_year: TimeKeeper.date_of_record.year, effective_date: TimeKeeper.date_of_record.beginning_of_year, - created_at: Date.new(2021, 10, 1) + submitted_at: DateTime.new(system_date.year, system_date.month, system_date.day) ) end @@ -146,6 +146,7 @@ end before do + allow(EnrollRegistry).to receive(:feature_enabled?).with(:qhp_application).and_return(true) allow(FinancialAssistanceRegistry).to receive(:feature_enabled?).with(:full_medicaid_determination_step).and_return(false) allow(FinancialAssistanceRegistry).to receive(:feature_enabled?).with(:indian_alaskan_tribe_details).and_return(false) allow(FinancialAssistanceRegistry).to receive(:feature_enabled?).with(:non_esi_mec_determination).and_return(true) @@ -162,7 +163,6 @@ describe '#call' do context 'success' do before do - allow(EnrollRegistry).to receive(:feature_enabled?).with(:qhp_application).and_return(true) # Build aptc_csr_eligibility for applicants to ensure they can build evidences application.applicants.each do |applicant| allow(applicant).to receive(:is_applying_coverage).and_return(true) @@ -216,11 +216,16 @@ applicant.build_aptc_eligibilities_evidences end application.save! + family.remove_instance_variable(:@fetch_latest_determined_application) + family.assign_latest_application_gid + family.save! end it 'should return failure if application hbx_id is passed' do result = subject.call({ application_hbx_id: application.hbx_id }) expect(result).to be_failure + family.reload + expect(family.eligibility_determination.application_gid).to eq application.to_global_id.uri.to_s end it 'builds non_esi_mec_evidence for active applicants before failure processing' do @@ -444,14 +449,9 @@ it 'handles the exception and returns failure' do result = subject.call({ application_hbx_id: application.hbx_id }) expect(result).to be_failure - expect(result.failure).to include('RRV process failed to publish event') + expect(result.failure).to match(/Failed to publish event for the application with hbx_id/) expect(result.failure).to include('Unexpected error') end - - it 'logs the error' do - expect_any_instance_of(Logger).to receive(:error).with(/RRV process failed to publish event/) - subject.call({ application_hbx_id: application.hbx_id }) - end end end end @@ -567,11 +567,6 @@ expect(result).to be_failure expect(result.failure).to include('Failed to save application: Validation failed: Field is required') end - - it 'logs the error' do - expect(subject.send(:rrv_logger)).to receive(:error).with(/Failed to save application/) - subject.send(:save_application, test_application) - end end end @@ -699,6 +694,11 @@ # Make one applicant valid and one invalid applicant.update_attributes!(ssn: "123456789") applicant2.update_attributes!(ssn: nil) # Invalid + application.applicants.each do |applicant| + allow(applicant).to receive(:is_applying_coverage).and_return(true) + applicant.build_aptc_eligibilities_evidences + end + application.save! end it 'handles mixed scenarios correctly' do @@ -733,7 +733,7 @@ result = subject.call({ application_hbx_id: application.hbx_id }) expect(result).to be_failure - expect(result.failure).to include('RRV process failed to publish event') + expect(result.failure).to match(/Failed to publish event for the application with hbx_id/) expect(result.failure).to include('Event building failed') # Since the error occurs during transform_and_validate_application (before evidence creation), diff --git a/components/financial_assistance/spec/domain/financial_assistance/operations/applications/shared/non_esi_evidence_request_spec.rb b/components/financial_assistance/spec/domain/financial_assistance/operations/applications/shared/non_esi_evidence_request_spec.rb index c7d51ff49b7..b7d7c273d18 100644 --- a/components/financial_assistance/spec/domain/financial_assistance/operations/applications/shared/non_esi_evidence_request_spec.rb +++ b/components/financial_assistance/spec/domain/financial_assistance/operations/applications/shared/non_esi_evidence_request_spec.rb @@ -94,6 +94,7 @@ def handle_validation_failure(errors) allow(aptc_csr_eligibility).to receive(:non_esi_mec_evidence).and_return(nil) allow(aptc_csr_eligibility).to receive(:determine_eligibility_state) + allow(operation).to receive(:update_family_determination).and_return(Dry::Monads::Success(true)) end describe '#call' do @@ -104,7 +105,6 @@ def handle_validation_failure(errors) before do allow(operation).to receive(:build_evidence_history).and_return(Dry::Monads::Success(true)) allow(operation).to receive(:transform_and_validate_application).and_return(Dry::Monads::Success(cv3_application)) - allow(operation).to receive(:update_family_determination).and_return(Dry::Monads::Success(true)) allow(operation).to receive(:build_event).and_return(Dry::Monads::Success(event)) end @@ -356,7 +356,7 @@ def handle_validation_failure(errors) it 'handles exceptions and returns failure' do result = operation.send(:transform_and_validate_application, application) expect(result).to be_failure - expect(result.failure).to include('test_process process failed') + expect(result.failure).to match(/Failed to publish event for the application with/) end end end From 21323df080f17700dcdc4a875f84a378ca76fed0 Mon Sep 17 00:00:00 2001 From: vishal kalletla Date: Mon, 8 Dec 2025 13:14:44 -0500 Subject: [PATCH 3/5] remove execution guards (#5941) * remove execution guards * fix logger name --- script/application_renewals/renewal_service.rb | 2 +- script/individual_market_eligibility/renewals/generate.rb | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/script/application_renewals/renewal_service.rb b/script/application_renewals/renewal_service.rb index 3eda24221f5..26384ff0058 100644 --- a/script/application_renewals/renewal_service.rb +++ b/script/application_renewals/renewal_service.rb @@ -67,7 +67,7 @@ class RenewalService def initialize(renewal_year:, primary_person_hbx_ids:) @renewal_year = renewal_year @primary_person_hbx_ids = primary_person_hbx_ids - @logger = Logger.new("#{Rails.root}/log/renewal_service_#{Time.now.strftime('%Y_%m_%d %H_%M_%S')}.log") + @logger = Logger.new("#{Rails.root}/log/renewal_service_#{Time.now.strftime('%Y_%m_%d_%H_%M_%S')}.log") end # Main entry point. diff --git a/script/individual_market_eligibility/renewals/generate.rb b/script/individual_market_eligibility/renewals/generate.rb index e81ff959f43..653a5a262fa 100644 --- a/script/individual_market_eligibility/renewals/generate.rb +++ b/script/individual_market_eligibility/renewals/generate.rb @@ -79,7 +79,7 @@ class Generate def initialize(renewal_year:, primary_person_hbx_ids:) @renewal_year = renewal_year @primary_person_hbx_ids = primary_person_hbx_ids - @qhp_logger = Logger.new("#{Rails.root}/log/qhp_renewal_generated_#{Time.now.strftime('%Y_%m_%d %H_%M_%S')}.log") + @qhp_logger = Logger.new("#{Rails.root}/log/qhp_renewal_generated_#{Time.now.strftime('%Y_%m_%d_%H_%M_%S')}.log") end def process @@ -165,8 +165,4 @@ def log_section(hbx_id) end end -if Rails.env.test? || (defined?(Rails) && $PROGRAM_NAME.include?('rails')) - Script::IndividualMarketEligibility::Renewals::Generate - .new(renewal_year: renewal_year_arg, primary_person_hbx_ids: primary_hbx_ids_arg) - .process -end \ No newline at end of file +Script::IndividualMarketEligibility::Renewals::Generate.new(renewal_year: renewal_year_arg, primary_person_hbx_ids: primary_hbx_ids_arg).process \ No newline at end of file From 7e73a3ceb3bcc643d8278d20a1908fb34f1c83c8 Mon Sep 17 00:00:00 2001 From: Jacob Kagon <69021620+jacobkagon@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:10:26 -0500 Subject: [PATCH 4/5] persist country of citizenship in the UI (#5945) * simplify country of citizenship retrieval * fix country of citizenship field autopopulating * rubocop fix --- .../_country_of_citizenship.html.erb | 2 +- .../_country_of_citizenship.html.erb | 2 +- .../_country_of_citizenship.html.erb_spec.rb | 54 +++++++++++++++++++ .../_country_of_citizenship.html.erb_spec.rb | 46 ++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 components/financial_assistance/spec/views/financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb_spec.rb create mode 100644 spec/views/insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb_spec.rb diff --git a/app/views/insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb b/app/views/insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb index 2d4131a787b..2ee98622969 100644 --- a/app/views/insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb +++ b/app/views/insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb @@ -2,7 +2,7 @@
<%= v.label :country_of_citizenship, l10n("insured.consumer_roles.docs_shared.country_of_citizenship"), class: "weight-n" %> <%= v.select :country_of_citizenship, - options_for_select(::VlpDocument::COUNTRIES_LIST, @country ||= l10n("insured.consumer_roles.docs_shared.country_of_citizenship")), + ::VlpDocument::COUNTRIES_LIST, { prompt: l10n("insured.consumer_roles.docs_shared.country_of_citizenship") }, { class: "select_tag", id: "country_of_citizenship" } %>
diff --git a/components/financial_assistance/app/views/financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb b/components/financial_assistance/app/views/financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb index 8749e4bbc8d..8dbe76ad89d 100644 --- a/components/financial_assistance/app/views/financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb +++ b/components/financial_assistance/app/views/financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb @@ -2,7 +2,7 @@
<%= v.label :country_of_citizenship, l10n("insured.consumer_roles.docs_shared.country_of_citizenship"), class: "weight-n" %> <%= v.select :country_of_citizenship, - options_for_select(::VlpDocument::COUNTRIES_LIST, @country ||= l10n("insured.consumer_roles.docs_shared.country_of_citizenship")), + ::VlpDocument::COUNTRIES_LIST, { prompt: l10n("insured.consumer_roles.docs_shared.country_of_citizenship") }, { class: "select_tag", id: "country_of_citizenship" } %>
diff --git a/components/financial_assistance/spec/views/financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb_spec.rb b/components/financial_assistance/spec/views/financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb_spec.rb new file mode 100644 index 00000000000..6b61629387e --- /dev/null +++ b/components/financial_assistance/spec/views/financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'financial_assistance/applicants/docs_shared/_country_of_citizenship.html.erb', type: :view do + let!(:application) do + FactoryBot.create(:application, + family_id: BSON::ObjectId.new, + aasm_state: 'draft', + effective_date: Date.today) + end + let!(:applicant) do + FactoryBot.create(:applicant, + application: application, + dob: Date.today - 40.years, + is_primary_applicant: true, + family_member_id: BSON::ObjectId.new, + country_of_citizenship: country_of_citizenship) + end + let(:country_of_citizenship) { 'United States' } + let(:form_builder) { double('FormBuilder') } + + before do + assign(:bs4, true) + allow(form_builder).to receive(:object).and_return(applicant) + assign(:applicant, applicant) + assign(:country, country_of_citizenship) + allow(view).to receive(:l10n).with("insured.consumer_roles.docs_shared.country_of_citizenship").and_return("Country of Citizenship") + allow(form_builder).to receive(:label).and_return(''.html_safe) + allow(form_builder).to receive(:select).and_return(''.html_safe) + end + + it 'renders the select field with proper options' do + render partial: 'financial_assistance/applicants/docs_shared/country_of_citizenship', locals: { v: form_builder } + expect(form_builder).to have_received(:select).with( + :country_of_citizenship, + ::VlpDocument::COUNTRIES_LIST, + { prompt: "Country of Citizenship" }, + { class: "select_tag", id: "country_of_citizenship" } + ) + end + + context 'when United States is selected' do + let(:country_of_citizenship) { 'United States' } + + it 'renders select with United States pre-selected' do + select_html = ''.html_safe + allow(form_builder).to receive(:select).and_return(select_html) + + render partial: 'financial_assistance/applicants/docs_shared/country_of_citizenship', locals: { v: form_builder } + expect(rendered).to include('selected="selected">United States') + end + end +end \ No newline at end of file diff --git a/spec/views/insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb_spec.rb b/spec/views/insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb_spec.rb new file mode 100644 index 00000000000..36b8a7a695e --- /dev/null +++ b/spec/views/insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'insured/consumer_roles/docs_shared/_country_of_citizenship.html.erb', type: :view do + let(:person) { FactoryBot.create(:person, :with_consumer_role) } + let(:consumer_role) { person.consumer_role } + let(:country_of_citizenship) { 'United States' } + let(:form_builder) { double('FormBuilder') } + + before do + allow(form_builder).to receive(:object).and_return(consumer_role) + assign(:country, country_of_citizenship) + allow(view).to receive(:l10n).with("insured.consumer_roles.docs_shared.country_of_citizenship").and_return("Country of Citizenship") + allow(form_builder).to receive(:label).and_return(''.html_safe) + allow(form_builder).to receive(:select).and_return(''.html_safe) + end + + context 'when bs4 is enabled' do + before do + assign(:bs4, true) + end + + it 'renders the select field with proper options' do + render partial: 'insured/consumer_roles/docs_shared/country_of_citizenship', locals: { v: form_builder } + expect(form_builder).to have_received(:select).with( + :country_of_citizenship, + ::VlpDocument::COUNTRIES_LIST, + { prompt: "Country of Citizenship" }, + { class: "select_tag", id: "country_of_citizenship" } + ) + end + + context 'when United States is selected' do + let(:country_of_citizenship) { 'United States' } + + it 'renders select with United States pre-selected' do + select_html = ''.html_safe + allow(form_builder).to receive(:select).and_return(select_html) + + render partial: 'insured/consumer_roles/docs_shared/country_of_citizenship', locals: { v: form_builder } + expect(rendered).to include('selected="selected">United States') + end + end + end +end \ No newline at end of file From 7c62f0ffd297a49e3232847bfec479f4a765eb2f Mon Sep 17 00:00:00 2001 From: Sri Harsha Date: Fri, 12 Dec 2025 11:36:45 -0500 Subject: [PATCH 5/5] CU-868gmnux2 :: Update workflow to auto build the image on the tags (#5944) --- .github/workflows/build-and-deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index e79083af198..415fde4e8b3 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -5,6 +5,8 @@ on: push: branches: - 'trunk' + tags: + - 'v*' pull_request: branches: - 'trunk'