diff --git a/app/controllers/admin/tasks_controller.rb b/app/controllers/admin/tasks_controller.rb index 9ad419a8df..f2881fd1e6 100644 --- a/app/controllers/admin/tasks_controller.rb +++ b/app/controllers/admin/tasks_controller.rb @@ -17,7 +17,7 @@ def show @notes = @claim.notes.automated.by_label(params[:name]) @task_pagination = Admin::TaskPagination.new(claim: @claim, current_task_name:) - render @task.name + render task_view(@task) end def create @@ -103,4 +103,16 @@ def set_banner_messages messages end + + def task_view(task) + policy = task.claim.policy + policy_path = policy.to_s.underscore + policy_scoped_task_name = "#{policy_path}/#{task.name}" + + if lookup_context.template_exists?(policy_scoped_task_name, [params[:controller]], false) + "admin/tasks/#{policy_scoped_task_name}" + else + task.name + end + end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 13e003510a..d9066c84d4 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -114,11 +114,17 @@ def process_one_login_identity_verification_callback(core_identity_jwt) ) end + ONE_LOGIN_TEST_USER = { + first_name: "TEST", + last_name: "USER", + date_of_birth: Date.new(1970, 1, 1) + } + def extract_data_from_jwt(jwt) if OneLoginSignIn.bypass? - first_name = "TEST" - last_name = "USER" - date_of_birth = Date.new(1970, 1, 1) + first_name = ONE_LOGIN_TEST_USER[:first_name] + last_name = ONE_LOGIN_TEST_USER[:last_name] + date_of_birth = ONE_LOGIN_TEST_USER[:date_of_birth] else validator = OneLogin::CoreIdentityValidator.new(jwt:) validator.call diff --git a/app/jobs/claim_verifier_job.rb b/app/jobs/claim_verifier_job.rb index f149cce24a..64b93cea86 100644 --- a/app/jobs/claim_verifier_job.rb +++ b/app/jobs/claim_verifier_job.rb @@ -1,8 +1,17 @@ class ClaimVerifierJob < ApplicationJob def perform(claim) - dqt_teacher_status = if claim.policy == Policies::EarlyYearsPayments - nil - elsif claim.has_dqt_record? + AutomatedChecks::ClaimVerifier.new( + claim: claim, + dqt_teacher_status: dqt_teacher_status(claim) + ).perform + end + + private + + def dqt_teacher_status(claim) + return if claim.policy == Policies::EarlyYearsPayments + + if claim.has_dqt_record? Dqt::Teacher.new(claim.dqt_teacher_status) else Dqt::Client.new.teacher.find( @@ -11,10 +20,5 @@ def perform(claim) nino: claim.national_insurance_number ) end - - AutomatedChecks::ClaimVerifier.new( - claim:, - dqt_teacher_status: - ).perform end end diff --git a/app/models/automated_checks/claim_verifiers/early_years_payments/identity.rb b/app/models/automated_checks/claim_verifiers/early_years_payments/identity.rb new file mode 100644 index 0000000000..59448bda4f --- /dev/null +++ b/app/models/automated_checks/claim_verifiers/early_years_payments/identity.rb @@ -0,0 +1,66 @@ +module AutomatedChecks + module ClaimVerifiers + module EarlyYearsPayments + class Identity < AutomatedChecks::ClaimVerifiers::Identity + def perform + return unless claim.eligibility.practitioner_journey_completed? + return unless awaiting_task?(TASK_NAME) + + if one_login_idv_match? + create_task(match: nil, passed: true) + elsif one_login_idv_partial_match? + create_task(match: :any, passed: nil) + + create_note( + body: <<-HTML + [GOV UK One Login Name] - Names partially match: +
+                Provider: "#{claim.eligibility.practitioner_name}"
+                GOV.UK One Login: "#{claim.onelogin_idv_full_name}"
+              
+ HTML + ) + elsif claim.one_login_idv_match? + create_task(match: nil, passed: false) + + create_note( + body: <<-HTML + [GOV UK One Login Name] - Names do not match: +
+                Provider: "#{claim.eligibility.practitioner_name}"
+                GOV.UK One Login: "#{claim.onelogin_idv_full_name}"
+              
+ HTML + ) + else + create_task(match: :none, passed: false) + + create_note( + body: <<-HTML + [GOV UK One Login] - IDV mismatch: +
+                GOV.UK One Login Name: "#{claim.onelogin_idv_full_name}"
+                GOV.UK One Login DOB: "#{claim.onelogin_idv_date_of_birth}"
+              
+ HTML + ) + end + end + + private + + def one_login_idv_match? + return false unless claim.one_login_idv_match? + + claim.eligibility.practitioner_and_provider_entered_names_match? + end + + def one_login_idv_partial_match? + return false unless claim.one_login_idv_match? + + claim.eligibility.practitioner_and_provider_entered_names_partial_match? + end + end + end + end +end diff --git a/app/models/claim.rb b/app/models/claim.rb index cf6482cbce..69236ea598 100644 --- a/app/models/claim.rb +++ b/app/models/claim.rb @@ -447,6 +447,10 @@ def one_login_idv_mismatch? !one_login_idv_name_match? || !one_login_idv_dob_match? end + def one_login_idv_match? + one_login_idv_name_match? && one_login_idv_dob_match? + end + def awaiting_provider_verification? return false unless has_further_education_policy? diff --git a/app/models/policies/early_years_payments.rb b/app/models/policies/early_years_payments.rb index 5092975df9..e44c8af0d0 100644 --- a/app/models/policies/early_years_payments.rb +++ b/app/models/policies/early_years_payments.rb @@ -7,7 +7,8 @@ module EarlyYearsPayments MIN_QA_THRESHOLD = 10 VERIFIERS = [ - AutomatedChecks::ClaimVerifiers::StudentLoanPlan + AutomatedChecks::ClaimVerifiers::StudentLoanPlan, + AutomatedChecks::ClaimVerifiers::EarlyYearsPayments::Identity ] # Attributes to delete from claims submitted before the current academic diff --git a/app/models/policies/early_years_payments/admin_tasks_presenter.rb b/app/models/policies/early_years_payments/admin_tasks_presenter.rb index f45a049c0a..be259ec421 100644 --- a/app/models/policies/early_years_payments/admin_tasks_presenter.rb +++ b/app/models/policies/early_years_payments/admin_tasks_presenter.rb @@ -15,6 +15,32 @@ def employment ["Start date", l(claim.eligibility.start_date)] ] end + + def identity_confirmation + [] + end + + def provider_entered_claimant_name + claim.eligibility.practitioner_name + end + + def one_login_claimant_name + claim.onelogin_idv_full_name + end + + def practitioner_journey_completed? + claim.eligibility.practitioner_journey_completed? + end + + def qualifications + [] + end + + def student_loan_plan + [ + ["Student loan plan", claim.student_loan_plan&.humanize] + ] + end end end end diff --git a/app/models/policies/early_years_payments/claim_checking_tasks.rb b/app/models/policies/early_years_payments/claim_checking_tasks.rb index a7b0a03584..f57c68905d 100644 --- a/app/models/policies/early_years_payments/claim_checking_tasks.rb +++ b/app/models/policies/early_years_payments/claim_checking_tasks.rb @@ -14,6 +14,7 @@ def initialize(claim) def applicable_task_names tasks = [] + tasks << "identity_confirmation" tasks << "student_loan_plan" if claim.submitted_without_slc_data? tasks diff --git a/app/models/policies/early_years_payments/eligibility.rb b/app/models/policies/early_years_payments/eligibility.rb index f006ab9e0d..3a2fcbaa48 100644 --- a/app/models/policies/early_years_payments/eligibility.rb +++ b/app/models/policies/early_years_payments/eligibility.rb @@ -37,6 +37,20 @@ def employment_task_available? def practitioner_name [practitioner_first_name, practitioner_surname].join(" ") end + + def practitioner_and_provider_entered_names_match? + practitioner_first_name.downcase == claim.onelogin_idv_first_name.downcase && + practitioner_surname.downcase == claim.onelogin_idv_last_name.downcase + end + + def practitioner_and_provider_entered_names_partial_match? + practitioner_first_name.downcase == claim.onelogin_idv_first_name.downcase || + practitioner_surname.downcase == claim.onelogin_idv_last_name.downcase + end + + def practitioner_journey_completed? + claim.submitted_at.present? + end end end end diff --git a/app/views/admin/tasks/early_years_payments/identity_confirmation.html.erb b/app/views/admin/tasks/early_years_payments/identity_confirmation.html.erb new file mode 100644 index 0000000000..c8146aa0c0 --- /dev/null +++ b/app/views/admin/tasks/early_years_payments/identity_confirmation.html.erb @@ -0,0 +1,60 @@ +<% content_for(:page_title) { page_title("Claim #{@claim.reference} identity confirmation check for #{@claim.policy.short_name}") } %> + +<% content_for :back_link do %> + <%= govuk_back_link href: admin_claim_tasks_path(@claim) %> +<% end %> + +<%= render "shared/error_summary", instance: @task, errored_field_id_overrides: { "passed": "task_passed_true" } if @task.errors.any? %> + +
+ <%= render claim_summary_view, claim: @claim, heading: "Identity confirmation" %> + +
+

<%= @current_task_name.humanize %>

+
+ +
+

+ <%= I18n.t( + "admin.tasks.identity_confirmation.title", + claim_full_name: @claim.full_name + ) %> +

+ + + + + + + + + + + + +
+ Provider entered claimant name + + <%= @tasks_presenter.provider_entered_claimant_name %> +
+ Claimant name from One login + + <%= @tasks_presenter.one_login_claimant_name %> +
+ + <% if @tasks_presenter.practitioner_journey_completed? %> + <% if @task.claim_verifier_match_any? && @task.passed.nil? %> + <%= render "form", task_name: "identity_confirmation", claim: @claim %> + <% else %> + <%= render "task_outcome", task: @task %> + <% end %> + <% else %> +
+ This task is not available until the claimant has submitted their + claim. +
+ <% end %> + + <%= render partial: "admin/task_pagination", locals: { task_pagination: @task_pagination } %> +
+
diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 01bf1afdda..2f9d9d340c 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -46,29 +46,6 @@ ], "note": "Create and update should be flagged but change is not different from existing behaviour, raising issue." }, - { - "warning_type": "Dynamic Render Path", - "warning_code": 15, - "fingerprint": "2e15a7fa4c8b8254b7724a1c5b8553cf4f7372f62b9401e1f5cbda1abe8c62ef", - "check_name": "Render", - "message": "Render path contains parameter value", - "file": "app/controllers/admin/tasks_controller.rb", - "line": 20, - "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", - "code": "render(action => Claim.includes(:tasks).find(params[:claim_id]).tasks.find_or_initialize_by(:name => params[:name]).name, {})", - "render_path": null, - "location": { - "type": "method", - "class": "Admin::TasksController", - "method": "show" - }, - "user_input": "params[:name]", - "confidence": "Weak", - "cwe_id": [ - 22 - ], - "note": "Constrained to valid input by routes" - }, { "warning_type": "SQL Injection", "warning_code": 0, @@ -115,6 +92,29 @@ ], "note": "" }, + { + "warning_type": "Dynamic Render Path", + "warning_code": 15, + "fingerprint": "9e2cf5f527443878fab8807fc6ca1af5a8f27690f312694489183624ab98d66d", + "check_name": "Render", + "message": "Render path contains parameter value", + "file": "app/controllers/admin/tasks_controller.rb", + "line": 20, + "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", + "code": "render(action => task_view(Claim.includes(:tasks).find(params[:claim_id]).tasks.find_or_initialize_by(:name => params[:name])), {})", + "render_path": null, + "location": { + "type": "method", + "class": "Admin::TasksController", + "method": "show" + }, + "user_input": "params[:name]", + "confidence": "Weak", + "cwe_id": [ + 22 + ], + "note": "" + }, { "warning_type": "SQL Injection", "warning_code": 0, @@ -139,6 +139,6 @@ "note": "" } ], - "updated": "2024-10-23 16:53:59 +0100", + "updated": "2024-10-30 16:55:54 +0000", "brakeman_version": "6.2.1" } diff --git a/config/locales/en.yml b/config/locales/en.yml index f1f2c1aec7..c554d64cfe 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1462,6 +1462,8 @@ en: task_questions: employment: title: Is the claimant still working at the current nursery above? + identity_confirmation: + title: "Do these names match?" early_years_payment_practitioner: journey_name: Claim an early years financial incentive payment - practitioner feedback_email: "help@opsteam.education.gov.uk" diff --git a/spec/features/admin/admin_ey_identity_task_spec.rb b/spec/features/admin/admin_ey_identity_task_spec.rb new file mode 100644 index 0000000000..002e718a73 --- /dev/null +++ b/spec/features/admin/admin_ey_identity_task_spec.rb @@ -0,0 +1,379 @@ +require "rails_helper" + +RSpec.describe "Admin EY identity task" do + around do |example| + travel_to DateTime.new(2024, 10, 30, 9, 0, 0) do + example.run + end + end + + context "when the practitioner hasn't completed their half of the claim" do + it "shows that the task is unavailable" do + claim = complete_provider_journey + + sign_in_as_service_operator + + visit admin_claim_tasks_path(claim) + + expect(task_status("Identity confirmation")).to eq("Incomplete") + + click_on "Confirm the claimant made the claim" + + expect(page).to have_content( + "Provider entered claimant name Bobby Bobberson" + ) + + expect(page).to have_content( + "This task is not available until the claimant has submitted their claim" + ) + end + end + + context "when the practitioner has completed their half of the claim" do + context "when OL IDV is a pass and the names match" do + it "passes the task" do + claim = complete_provider_journey + + complete_practitioner_journey( + claim: claim, + date_of_birth: Date.new(1986, 1, 1), + one_login_first_name: "Bobby", + one_login_last_name: "Bobberson", + one_login_date_of_birth: Date.new(1986, 1, 1) + ) + + sign_in_as_service_operator + + visit admin_claim_tasks_path(claim) + + expect(task_status("Identity confirmation")).to eq("Passed") + + click_on "Confirm the claimant made the claim" + + expect(page).to have_content( + "Provider entered claimant name Bobby Bobberson" + ) + + expect(page).to have_content( + "Claimant name from One login Bobby Bobberson" + ) + + expect(page).to have_content( + "This task was performed by GOV.UK One Login on " \ + "30 October 2024 9:00am" + ) + end + end + + context "when OL IDV is a pass and the names don't match" do + context "when the names are a parital match" do + let(:claim) { complete_provider_journey } + + before do + complete_practitioner_journey( + claim: claim, + one_login_first_name: "Robby", + one_login_last_name: "Bobberson", + one_login_date_of_birth: Date.new(1986, 1, 1), + date_of_birth: Date.new(1986, 1, 1) + ) + + sign_in_as_service_operator + end + + it "shows the task as a partial match" do + visit admin_claim_tasks_path(claim) + + expect(task_status("Identity confirmation")).to eq("Partial match") + + click_on "Confirm the claimant made the claim" + + expect(page).to have_content( + "Provider entered claimant name Bobby Bobberson" + ) + + expect(page).to have_content( + "Claimant name from One login Robby Bobberson" + ) + + expect(page).to have_content( + "[GOV UK One Login Name] - Names partially match" + ) + + expect(page).to have_content('Provider: "Bobby Bobberson"') + + expect(page).to have_content('GOV.UK One Login: "Robby Bobberson"') + end + + it "allows the admin to mark the task as passed" do + visit admin_claim_task_path(claim, name: "identity_confirmation") + + choose "Yes" + + click_on "Save and continue" + + visit admin_claim_task_path(claim, name: "identity_confirmation") + + expect(page).to have_content( + "This task was performed by Aaron Admin" + ) + + visit admin_claim_tasks_path(claim) + + expect(task_status("Identity confirmation")).to eq("Passed") + end + + it "allows the admin to mark the task as failed" do + visit admin_claim_task_path(claim, name: "identity_confirmation") + + choose "No" + + click_on "Save and continue" + + visit admin_claim_task_path(claim, name: "identity_confirmation") + + expect(page).to have_content( + "This task was performed by Aaron Admin" + ) + + visit admin_claim_tasks_path(claim) + + expect(task_status("Identity confirmation")).to eq("Failed") + end + end + + context "when the names don't match" do + let(:claim) { complete_provider_journey } + + before do + complete_practitioner_journey( + claim: claim, + one_login_first_name: "Robby", + one_login_last_name: "Robberson", + one_login_date_of_birth: Date.new(1986, 1, 1), + date_of_birth: Date.new(1986, 1, 1) + ) + + sign_in_as_service_operator + end + + it "shows the task as failed" do + visit admin_claim_tasks_path(claim) + + expect(task_status("Identity confirmation")).to eq("Failed") + + click_on "Confirm the claimant made the claim" + + expect(page).to have_content( + "Provider entered claimant name Bobby Bobberson" + ) + + expect(page).to have_content( + "Claimant name from One login Robby Robberson" + ) + + expect(page).to have_content( + "[GOV UK One Login Name] - Names do not match" + ) + + expect(page).to have_content('Provider: "Bobby Bobberson"') + + expect(page).to have_content('GOV.UK One Login: "Robby Robberson"') + end + + it "doesn't allow the admin to complete the task" do + visit admin_claim_task_path(claim, name: "identity_confirmation") + + expect(page).not_to have_button("Save and continue") + end + end + end + + context "when OL IDV is a fail" do + it "fails the task" do + claim = complete_provider_journey + + complete_practitioner_journey( + claim: claim, + one_login_first_name: "Bobby", + one_login_last_name: "Bobberson", + date_of_birth: Date.new(1986, 1, 11), + one_login_date_of_birth: Date.new(1986, 1, 1) + ) + + sign_in_as_service_operator + + visit admin_claim_tasks_path(claim) + + expect(task_status("Identity confirmation")).to eq("Failed") + + click_on "Confirm the claimant made the claim" + + expect(page).to have_content( + "[GOV UK One Login] - IDV mismatch:" + ) + + expect(page).to have_content('GOV.UK One Login Name: "Bobby Bobberson"') + + expect(page).to have_content('GOV.UK One Login DOB: "1986-01-01"') + + expect(page).not_to have_button("Save and continue") + end + end + end + + def complete_provider_journey + create(:journey_configuration, :early_years_payment_provider_start) + + create(:journey_configuration, :early_years_payment_provider_authenticated) + + nursery = create( + :eligible_ey_provider, + primary_key_contact_email_address: "johndoe@example.com", + secondary_contact_email_address: "janedoe@example.com" + ) + + visit landing_page_path( + Journeys::EarlyYearsPayment::Provider::Start::ROUTING_NAME + ) + + click_link "Start now" + + fill_in "Email address", with: "johndoe@example.com" + click_on "Submit" + + mail = ActionMailer::Base.deliveries.last + magic_link = mail[:personalisation].unparsed_value[:magic_link] + + visit magic_link + + check( + "I confirm that I have obtained consent from my employee and have " \ + "provided them with the relevant privacy notice." + ) + click_button "Continue" + + choose nursery.nursery_name + click_button "Continue" + + fill_in "claim-paye-reference-field", with: "123/123456SE90" + click_button "Continue" + + fill_in "First name", with: "Bobby" + fill_in "Last name", with: "Bobberson" + click_button "Continue" + + date = Date.yesterday + fill_in("Day", with: date.day) + fill_in("Month", with: date.month) + fill_in("Year", with: date.year) + click_button "Continue" + + # /early-years-payment-provider/child-facing + choose "Yes" + click_button "Continue" + + # /early-years-payment-provider/returner + choose "Yes" + click_button "Continue" + + # /early-years-payment-provider/returner-worked-with-children + choose "Yes" + click_button "Continue" + + # /early-years-payment-provider/returner-contract-type + choose "casual or temporary" + click_button "Continue" + + # /early-years-payment-provider/employee-email + fill_in( + "claim-practitioner-email-address-field", + with: "practitioner@example.com" + ) + + click_button "Continue" + + # /early-years-payment-provider/check-your-answers + fill_in "claim-provider-contact-name-field", with: "John Doe" + perform_enqueued_jobs { click_button "Accept and send" } + + Claim.last + end + + def complete_practitioner_journey( + claim:, + date_of_birth:, + one_login_first_name:, + one_login_last_name:, + one_login_date_of_birth: + ) + stub_const( + "OmniauthCallbacksController::ONE_LOGIN_TEST_USER", + { + first_name: one_login_first_name, + last_name: one_login_last_name, + date_of_birth: one_login_date_of_birth + } + ) + + create(:journey_configuration, :early_years_payment_practitioner) + + visit "/early-years-payment-practitioner/find-reference?skip_landing_page=true&email=practitioner@example.com" + + fill_in "Claim reference number", with: claim.reference + + click_button "Submit" + + sign_in_with_one_login + + click_on "Continue" + + expect(page).to have_content("Personal details") + fill_in "Day", with: date_of_birth.day + fill_in "Month", with: date_of_birth.month + fill_in "Year", with: date_of_birth.year + fill_in "National Insurance number", with: "PX321499A" + click_on "Continue" + + expect(page).to have_content("What is your address?") + fill_in "House number or name", with: "57" + fill_in "Building and street", with: "Walthamstow Drive" + fill_in "Town or city", with: "Derby" + fill_in "County", with: "City of Derby" + fill_in "Postcode", with: "DE22 4BS" + click_on "Continue" + + expect(page).to have_content("Your email address") + fill_in "claim-email-address-field", with: "johndoe@example.com" + click_on "Continue" + + expect(page).to have_content("Enter the 6-digit passcode") + mail = ActionMailer::Base.deliveries.last + otp_in_mail_sent = mail[:personalisation].unparsed_value[:one_time_password] + fill_in "claim-one-time-password-field", with: otp_in_mail_sent + click_on "Confirm" + + expect(page).to have_content("Would you like to provide your mobile number?") + choose "No" + click_on "Continue" + + fill_in "Name on your account", with: "#{claim.first_name} #{claim.surname}" + fill_in "Sort code", with: "123456" + fill_in "Account number", with: "87654321" + click_on "Continue" + + expect(page).to have_text(I18n.t("forms.gender.questions.payroll_gender")) + choose "Male" + click_on "Continue" + + expect(page).to have_content("Check your answers before submitting this claim") + + perform_enqueued_jobs { click_on "Accept and send" } + end + + def task_status(task_name) + find("h2.app-task-list__section", text: task_name) + .find(:xpath, 'following-sibling::ul//strong[contains(@class, "govuk-tag")]') + .text + end +end diff --git a/spec/jobs/claim_verifier_job_spec.rb b/spec/jobs/claim_verifier_job_spec.rb index 46163c322a..311a76d91d 100644 --- a/spec/jobs/claim_verifier_job_spec.rb +++ b/spec/jobs/claim_verifier_job_spec.rb @@ -40,6 +40,16 @@ end end + context "when the claim is for EarlyYearsPayments" do + let(:claim) { build(:claim, policy: Policies::EarlyYearsPayments) } + + it "performs the verifier job but doesn't request dqt info" do + expect(AutomatedChecks::ClaimVerifier).to receive(:new) + expect(dbl).not_to receive(:find) + described_class.new.perform(claim) + end + end + context "when the claim does not have a DQT record payload" do let(:dqt_teacher_status) { nil } diff --git a/spec/support/admin_view_claim_feature_shared_examples.rb b/spec/support/admin_view_claim_feature_shared_examples.rb index d18b84f57c..ff61067d26 100644 --- a/spec/support/admin_view_claim_feature_shared_examples.rb +++ b/spec/support/admin_view_claim_feature_shared_examples.rb @@ -163,7 +163,7 @@ def expect_page_to_have_policy_sections(policy) when Policies::FurtherEducationPayments ["Identity confirmation", "Provider verification", "Student loan plan", "Decision"] when Policies::EarlyYearsPayments - ["Student loan plan", "Decision"] + ["Identity confirmation", "Student loan plan", "Decision"] else raise "Unimplemented policy: #{policy}" end