From f777c09aa12205f93b0afb01b683037a3efb4e32 Mon Sep 17 00:00:00 2001 From: Utkarsh Shukla Date: Wed, 8 Nov 2023 13:15:25 -0500 Subject: [PATCH] Run eligibility check feature (#2463) * Eligibility check backend changes (#2460) * adds run eligibility check controller action and routes * adds specs * adds run eligibility route * Eligibility check authorization (#2465) * adds pundit policy for admin access only * fixes typo * fixes spec failures * Run eligibility check front (#2464) * Implement eligibility checks in front-end UI * removed placeholder button * Fixed failing specs * fixed mistyped javascript * Rails renders button_tag as a form with an input field, why selector was not being found. Working specs * Added Specs for Eligibility Check Partial * Moved Eligibility Spec to its correct directory * Added Translations for Run Eligibility button * resolved rubocop errors for files that aren't slim templates * fixed more rubocop issues, more are failing * removed comment * fixed route mispell * Added translations for the response text * fixed UI breaking from translations update --------- Co-authored-by: Utkarsh Shukla * adds condition to display the button during OE period * PR revisions * fixes the row alignment issue --------- Co-authored-by: TonyHasIdeas <118292503+TonyHasIdeas@users.noreply.github.com> Co-authored-by: Antonio Irizarry Co-authored-by: Sri Harsha --- app/assets/stylesheets/custom.scss.erb | 19 +++ .../v1/cards/_eligibility_check.html.slim | 113 ++++++++++++++++++ .../employers/employer_profiles_controller.rb | 21 +++- .../employer_profile_policy.rb | 6 + .../my_account/_home_tab.html.slim | 2 + components/benefit_sponsors/config/routes.rb | 1 + .../employer_profiles_controller_spec.rb | 23 ++++ .../policies/employer_profile_policy_spec.rb | 15 ++- .../my_account/_home_tab.html.erb_spec.rb | 50 +++++++- db/seedfiles/translations/en/cca/employer.rb | 6 + .../cards/_eligibility_check.html.erb_spec.rb | 50 ++++++++ 11 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 app/views/ui-components/v1/cards/_eligibility_check.html.slim create mode 100644 spec/views/ui-components/v1/cards/_eligibility_check.html.erb_spec.rb diff --git a/app/assets/stylesheets/custom.scss.erb b/app/assets/stylesheets/custom.scss.erb index 869d3d5707e..43129e94b48 100644 --- a/app/assets/stylesheets/custom.scss.erb +++ b/app/assets/stylesheets/custom.scss.erb @@ -4976,3 +4976,22 @@ input:required:valid { color: gray; background-color: #80808026; } + +.eligibility-status-text{ + font-size: 18px; +} +.is-eligible-checkmark{color: green;} +.eligibility-response-close-icon{ + font-size: 24px; + font-weight: bold; + display: block; + float: right; +} +.run-eligibility-check-response{ + padding-top: 20px; + display: block; + width: 100%; +} +.eligibility-response-close-icon{ + cursor: pointer; +} diff --git a/app/views/ui-components/v1/cards/_eligibility_check.html.slim b/app/views/ui-components/v1/cards/_eligibility_check.html.slim new file mode 100644 index 00000000000..b866bcbe7d3 --- /dev/null +++ b/app/views/ui-components/v1/cards/_eligibility_check.html.slim @@ -0,0 +1,113 @@ +.panel.panel-default#employee-enrollments + .panel-body + .run-eligibility-check-btn-container + = button_to l10n("employers.plan_years.eligibility_button_text"), '/', class: "btn btn-default", id: "eligibilityCheckButton", data: { url: profiles_employers_employer_profile_run_eligibility_check_path(@employer_profile) } + .col-xs-12.loading.run-eligibility-processing(style="display: none;") + i.fa.fa-spinner.fa-spin.fa-2x + .run-eligibility-check-response-container(style="display: none;") + span.eligibility-response-close-icon + | X + .run-eligibility-check-response + p.eligibility-status-text.minimum-participation + => l10n("employers.plan_years.minimum_participation_text") + |    + span.not-eligible + | ❌ + span.is-eligible-checkmark style="display:none;" + | ✔ + p.eligibility-status-text.non-business-owner-eligibility-count + => l10n("employers.plan_years.non-business_owner_eligibility_count_text") + |    + span.not-eligible + | ❌ + span.is-eligible-checkmark style="display:none;" + | ✔ + p.eligibility-status-text.minimum-eligible-member-count + => l10n("employers.plan_years.minimum_eligible_member_count_text") + |    + span.not-eligible + | ❌ + span.is-eligible-checkmark style="display:none;" + | ✔ + + +javascript: + document.getElementById('eligibilityCheckButton').addEventListener('click', function() { + this.disabled = true; + + // Show Spinner + $('#eligibilityCheckButton').hide(); + $('.run-eligibility-processing').show(); + // Fetch the URL from the data attribute + var url = this.getAttribute('data-url'); + + $.ajax({ + type: 'GET', + data: {}, + url: url, + }).done(function(response) { + $('.run-eligibility-processing').hide(); + $('.run-eligibility-check-response-container').show(); + + interpretResponse = interpretValidation(response); + updateEligibilityUI(interpretResponse); + }); + + function interpretValidation(response) { + // List of all attributes we are checking + const attributes = ['minimum_participation_rule', 'non_business_owner_enrollment_count', 'minimum_eligible_member_count']; + + let result = {}; + + for (let attribute of attributes) { + // If the attribute exists in the response and its value is 'validated successfully', then it's valid. + if (response[attribute] && response[attribute] === 'validated successfully') { + result[attribute] = true; + } else if (response[attribute]) { // If the attribute exists in the response and its value isn't 'validated successfully', then it's invalid. + result[attribute] = false; + } else { // If the attribute doesn't exist in the response, then it's valid. + result[attribute] = true; + } + } + return result; + } + }); + + function updateEligibilityUI(interpretedResponse) { + // Getting all the p tags within the parent div + const eligibilityStatusElements = document.querySelectorAll(".run-eligibility-check-response .eligibility-status-text"); + + eligibilityStatusElements.forEach(element => { + // Based on the class of the p tag, deciding which attribute we are currently processing + let attribute; + if (element.classList.contains("minimum-participation")) { + attribute = "minimum_participation_rule"; + } else if (element.classList.contains("non-business-owner-eligibility-count")) { + attribute = "non_business_owner_enrollment_count"; + } else if (element.classList.contains("minimum-eligible-member-count")) { + attribute = "minimum_eligible_member_count"; + } + + if (interpretedResponse[attribute]) { + // If true, show the checkmark and hide the cross + element.querySelector('.is-eligible-checkmark').style.display = 'inline'; + element.querySelector('.not-eligible').style.display = 'none'; + } else { + // If false, show the cross and hide the checkmark + element.querySelector('.is-eligible-checkmark').style.display = 'none'; + element.querySelector('.not-eligible').style.display = 'inline'; + } + }); + } + + document.querySelector('.eligibility-response-close-icon').addEventListener('click', function() { + // Hide the container + $('.run-eligibility-check-response-container').hide(); + + // Hide the 'is-eligible-checkmark' and show the 'not-eligible' icon (original state) + $('.is-eligible-checkmark').hide(); + $('.not-eligible').show(); + + // Show the button and activate + $('#eligibilityCheckButton').show().prop('disabled', false); + }); diff --git a/components/benefit_sponsors/app/controllers/benefit_sponsors/profiles/employers/employer_profiles_controller.rb b/components/benefit_sponsors/app/controllers/benefit_sponsors/profiles/employers/employer_profiles_controller.rb index 6663353547c..664da6b8326 100644 --- a/components/benefit_sponsors/app/controllers/benefit_sponsors/profiles/employers/employer_profiles_controller.rb +++ b/components/benefit_sponsors/app/controllers/benefit_sponsors/profiles/employers/employer_profiles_controller.rb @@ -3,7 +3,7 @@ module Profiles module Employers class EmployerProfilesController < ::BenefitSponsors::ApplicationController - before_action :find_employer, only: [:show, :inbox, :bulk_employee_upload, :export_census_employees, :coverage_reports, :download_invoice, :show_invoice, :estimate_cost] + before_action :find_employer, only: [:show, :inbox, :bulk_employee_upload, :export_census_employees, :coverage_reports, :download_invoice, :show_invoice, :estimate_cost, :run_eligibility_check] before_action :load_group_enrollments, only: [:coverage_reports], if: :is_format_csv? before_action :check_and_download_invoice, only: [:download_invoice, :show_invoice] before_action :wells_fargo_sso, only: [:show] @@ -61,6 +61,7 @@ def show # TODO - Each when clause should be a seperate action. if @benefit_sponsorship.present? @broker_agency_accounts = @benefit_sponsorship.broker_agency_accounts @current_plan_year = @benefit_sponsorship.submitted_benefit_application(include_term_pending: false) + @business_policy = business_policy_for(@current_plan_year) end collect_and_sort_invoices(params[:sort_order]) @@ -87,6 +88,19 @@ def coverage_reports end end + def run_eligibility_check + authorize @employer_profile + benefit_sponsorship = @employer_profile.latest_benefit_sponsorship + benefit_application = benefit_sponsorship.submitted_benefit_application(include_term_pending: false) + business_policy = business_policy_for(benefit_application) + eligibility_hash = if business_policy.is_satisfied?(benefit_application) + business_policy.success_results + else + business_policy.fail_results + end + render :json => eligibility_hash + end + def export_census_employees authorize @employer_profile respond_to do |format| @@ -316,6 +330,11 @@ def user_not_authorized(exception) session[:custom_url] = main_app.new_user_registration_path unless current_user super end + + def business_policy_for(benefit_application) + enrollment_eligibility_policy = BenefitSponsors::BenefitApplications::AcaShopEnrollmentEligibilityPolicy.new + enrollment_eligibility_policy.business_policies_for(benefit_application, :end_open_enrollment) + end end end end diff --git a/components/benefit_sponsors/app/policies/benefit_sponsors/employer_profile_policy.rb b/components/benefit_sponsors/app/policies/benefit_sponsors/employer_profile_policy.rb index 15054527da6..bc5ceda697d 100644 --- a/components/benefit_sponsors/app/policies/benefit_sponsors/employer_profile_policy.rb +++ b/components/benefit_sponsors/app/policies/benefit_sponsors/employer_profile_policy.rb @@ -59,5 +59,11 @@ def can_list_enrollments? def can_modify_employer? user.person.hbx_staff_role.permission.modify_employer end + + def run_eligibility_check? + return false unless user.present? + + user.has_hbx_staff_role? + end end end diff --git a/components/benefit_sponsors/app/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/_home_tab.html.slim b/components/benefit_sponsors/app/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/_home_tab.html.slim index 64477a2dd67..f8f2006b6e9 100644 --- a/components/benefit_sponsors/app/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/_home_tab.html.slim +++ b/components/benefit_sponsors/app/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/_home_tab.html.slim @@ -6,6 +6,8 @@ - if @current_plan_year.present? = render partial: 'ui-components/v1/cards/employee_enrollments' = render partial: 'ui-components/v1/cards/plan_year' + - if current_user.has_hbx_staff_role? && @current_plan_year.open_enrollment_contains?(TimeKeeper.date_of_record) + = render partial: 'ui-components/v1/cards/eligibility_check' - @current_plan_year.benefit_groups.each do |bg| = render partial: 'ui-components/v1/cards/benefit_groups', locals: { bg: bg } - else diff --git a/components/benefit_sponsors/config/routes.rb b/components/benefit_sponsors/config/routes.rb index af0a3b66178..a47c86e3ac7 100644 --- a/components/benefit_sponsors/config/routes.rb +++ b/components/benefit_sponsors/config/routes.rb @@ -35,6 +35,7 @@ post :bulk_employee_upload get :coverage_reports get :estimate_cost + get :run_eligibility_check collection do get :generate_sic_tree get :show_pending diff --git a/components/benefit_sponsors/spec/controllers/benefit_sponsors/profiles/employers/employer_profiles_controller_spec.rb b/components/benefit_sponsors/spec/controllers/benefit_sponsors/profiles/employers/employer_profiles_controller_spec.rb index 8cdde3e3b2c..f77c189becf 100644 --- a/components/benefit_sponsors/spec/controllers/benefit_sponsors/profiles/employers/employer_profiles_controller_spec.rb +++ b/components/benefit_sponsors/spec/controllers/benefit_sponsors/profiles/employers/employer_profiles_controller_spec.rb @@ -144,5 +144,28 @@ module BenefitSponsors expect(response).to have_http_status(:success) end end + + describe "GET run_eligibility_check" do + let(:admin_user) { FactoryGirl.create(:user, :with_hbx_staff_role, :person => person)} + let!(:employees) { FactoryGirl.create_list(:census_employee, 2, employer_profile: employer_profile, benefit_sponsorship: benefit_sponsorship)} + let(:business_policy) { instance_double("some_policy", success_results: { business_rule: "validated successfully" })} + + before do + sign_in admin_user + allow(subject).to receive(:business_policy_for).and_return(business_policy) + allow(business_policy).to receive(:is_satisfied?).and_return(true) + get :run_eligibility_check, employer_profile_id: benefit_sponsor.profiles.first.id + allow(employer_profile).to receive(:active_benefit_sponsorship).and_return benefit_sponsorship + end + + it "should return http success" do + expect(response).to have_http_status(:success) + end + + it 'responds with a json content type' do + expect(response.content_type).to include('application/json') + expect(JSON.parse(response.body, symoblize_names: true)).to include("business_rule" => "validated successfully") + end + end end end diff --git a/components/benefit_sponsors/spec/policies/employer_profile_policy_spec.rb b/components/benefit_sponsors/spec/policies/employer_profile_policy_spec.rb index 63e62ce7f7d..5792e663904 100644 --- a/components/benefit_sponsors/spec/policies/employer_profile_policy_spec.rb +++ b/components/benefit_sponsors/spec/policies/employer_profile_policy_spec.rb @@ -57,5 +57,18 @@ module BenefitSponsors it_behaves_like 'should not permit for person with active employer staff role', :coverage_reports? it_behaves_like 'should not permit for person with active employer staff role', :updateable? end + + context 'for a user with admin role' do + let(:user) { FactoryGirl.create(:user, :with_hbx_staff_role, person: person) } + + shared_examples_for 'should permit for a user with hbx staff role' do |policy_type| + it 'should permit for admin role' do + expect(policy.send(policy_type)).to be true + end + end + + it_behaves_like 'should permit for a user with hbx staff role', :show? + it_behaves_like 'should permit for a user with hbx staff role', :run_eligibility_check? + end end -end \ No newline at end of file +end diff --git a/components/benefit_sponsors/spec/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/_home_tab.html.erb_spec.rb b/components/benefit_sponsors/spec/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/_home_tab.html.erb_spec.rb index 19611a0af5b..73ac3f8394e 100644 --- a/components/benefit_sponsors/spec/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/_home_tab.html.erb_spec.rb +++ b/components/benefit_sponsors/spec/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/_home_tab.html.erb_spec.rb @@ -3,12 +3,19 @@ require "rails_helper" RSpec.describe "components/benefit_sponsors/app/views/benefit_sponsors/profiles/employers/employer_profiles/my_account/home_tab.html.slim" do + context "employer profile dashboard with current plan year" do let(:start_on){TimeKeeper.date_of_record.beginning_of_year} let(:end_on){TimeKeeper.date_of_record.end_of_year} let(:end_on_negative){ TimeKeeper.date_of_record.beginning_of_year - 2.years } let(:active_employees) { double("CensusEmployee", count: 10) } + let(:mock_user) do + instance_double( + "User", + has_hbx_staff_role?: false + ) + end def new_organization @@ -189,7 +196,8 @@ def plan_year aasm_state: 'draft', predecessor_id: nil, employee_participation_ratio_minimum: 0.75, - employer_profile: double(census_employees: double(active: active_employees)) + employer_profile: double(census_employees: double(active: active_employees)), + open_enrollment_contains?: true ) end @@ -270,11 +278,13 @@ def reference_product end before :each do + view.extend BenefitSponsors::Engine.routes.url_helpers allow(::BenefitSponsors::Services::SponsoredBenefitCostEstimationService).to receive(:new).and_return(cost_estimator) allow(cost_estimator).to receive(:calculate_estimates_for_home_display).and_return(estimator) allow(view).to receive(:pundit_class).and_return(double("EmployerProfilePolicy", updateable?: true)) allow(view).to receive(:policy_helper).and_return(double("EmployerProfilePolicy", updateable?: true)) + assign :employer_profile, employer_profile assign :hbx_enrollments, [hbx_enrollment] assign :current_plan_year, employer_profile.published_plan_year @@ -285,6 +295,7 @@ def reference_product context "when employer setting is enabled" do it "should display the employer external links advertisement" do + allow(view).to receive(:current_user).and_return(mock_user) EnrollRegistry[:add_external_links].feature.stub(:is_enabled).and_return(true) EnrollRegistry[:add_external_links].setting(:employer_display).stub(:item).and_return(true) @@ -297,6 +308,7 @@ def reference_product context "when employer setting is disabled" do it "should not display the employer external links advertisement" do + allow(view).to receive(:current_user).and_return(mock_user) EnrollRegistry[:add_external_links].feature.stub(:is_enabled).and_return(true) EnrollRegistry[:add_external_links].setting(:employer_display).stub(:item).and_return(false) @@ -306,5 +318,41 @@ def reference_product expect(rendered).not_to include("href=\"https://www.mahealthconnector.org/business/employers/connectwell-for-employers") end end + + context "when user is an admin" do + before do + allow(mock_user).to receive(:has_hbx_staff_role?).and_return(true) + allow(view).to receive(:current_user).and_return(mock_user) + render "benefit_sponsors/profiles/employers/employer_profiles/my_account/home_tab" + end + + it "renders the 'Run Eligibility Check' button" do + expect(rendered).to have_selector('input#eligibilityCheckButton') + end + end + + context "when user is not an admin" do + before do + allow(mock_user).to receive(:has_hbx_staff_role?).and_return(false) + allow(view).to receive(:current_user).and_return(mock_user) + render "benefit_sponsors/profiles/employers/employer_profiles/my_account/home_tab" + end + + it "does not render the 'Run Eligibility Check' button" do + expect(rendered).not_to have_selector('input#eligibilityCheckButton') + end + end + + context "when current plan year is in open enrollment period" do + before do + allow(mock_user).to receive(:has_hbx_staff_role?).and_return(true) + allow(view).to receive(:current_user).and_return(mock_user) + render "benefit_sponsors/profiles/employers/employer_profiles/my_account/home_tab" + end + + it "renders the 'Run Eligibility Check' button" do + expect(rendered).to have_selector('input#eligibilityCheckButton') + end + end end end diff --git a/db/seedfiles/translations/en/cca/employer.rb b/db/seedfiles/translations/en/cca/employer.rb index 34ea73c549e..b2e6c7f68d6 100644 --- a/db/seedfiles/translations/en/cca/employer.rb +++ b/db/seedfiles/translations/en/cca/employer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # MA Census Employee Translations EMPLOYER_TRANSLATIONS = { :'en.employers.plan_years.reference_plan_details' => "Reference Plan Details", @@ -43,6 +45,10 @@ :'en.employers.plan_years.employee_cost_details_description_part1' => "Details of the employee costs for your members are shown on the", :'en.employers.plan_years.employee_cost_details_description_part2' => "Values are based on benefit type, contribution amounts, and reference plan chosen.", :'en.employers.plan_years.estimated_employee_costs' => "Estimated Employee Costs", + :'en.employers.plan_years.eligibility_button_text' => "Run Eligibility Check", + :'en.employers.plan_years.minimum_participation_text' => 'Minimum Participation', + :'en.employers.plan_years.non-business_owner_eligibility_count_text' => 'Non-Business Owner Eligibility Count', + :'en.employers.plan_years.minimum_eligible_member_count_text' => 'Minimum Eligible Member Count', :'en.employers.registration.kind' => 'Kind *', :'en.employers.registration.address' => 'Address *', :'en.employers.registration.city' => 'City *', diff --git a/spec/views/ui-components/v1/cards/_eligibility_check.html.erb_spec.rb b/spec/views/ui-components/v1/cards/_eligibility_check.html.erb_spec.rb new file mode 100644 index 00000000000..345167a1951 --- /dev/null +++ b/spec/views/ui-components/v1/cards/_eligibility_check.html.erb_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "ui-components/v1/cards/_eligibility_check.html.slim" do + context "when the eligibility partial is rendered" do + + let(:employer_profile) do + instance_double( + "EmployerProfile" + ) + end + + before do + # Extend view with necessary url helpers + view.extend BenefitSponsors::Engine.routes.url_helpers + + # Mock EmployerProfile and assign it to view's instance variable + employer_profile = instance_double("EmployerProfile", id: 1) + assign(:employer_profile, employer_profile) + + # Render the partial with the assigned employer_profile + render "ui-components/v1/cards/eligibility_check" + end + + it "displays the 'Run Eligibility Check' button" do + expect(rendered).to have_selector('input#eligibilityCheckButton') + end + + it "contains the loading icon with the appropriate classes" do + expect(rendered).to have_selector(".col-xs-12.loading.run-eligibility-processing i.fa.fa-spinner.fa-spin.fa-2x", visible: false) + end + + it "contains the eligibility response container which is hidden by default" do + expect(rendered).to have_selector('.run-eligibility-check-response-container[style="display: none;"]', visible: false) + end + + it "contains the Minimum Participation status text" do + expect(rendered).to have_selector("p.eligibility-status-text.minimum-participation", visible: false) + end + + it "contains the Non-Business Owner Eligibility Count status text" do + expect(rendered).to have_selector("p.eligibility-status-text.non-business-owner-eligibility-count", visible: false) + end + + it "contains the Minimum Eligible Member Count status text" do + expect(rendered).to have_selector("p.eligibility-status-text.minimum-eligible-member-count", visible: false) + end + end +end