diff --git a/app/controllers/claims_controller.rb b/app/controllers/claims_controller.rb index 48c578d128..2813617f68 100644 --- a/app/controllers/claims_controller.rb +++ b/app/controllers/claims_controller.rb @@ -12,6 +12,7 @@ class ClaimsController < BasePublicController before_action :persist_claim, only: [:new, :create] before_action :handle_magic_link, only: [:new], if: -> { journey.start_with_magic_link? } + include AuthorisedSlugs include FormSubmittable include ClaimsFormCallbacks diff --git a/app/controllers/concerns/authorised_slugs.rb b/app/controllers/concerns/authorised_slugs.rb new file mode 100644 index 0000000000..540ba9d4d5 --- /dev/null +++ b/app/controllers/concerns/authorised_slugs.rb @@ -0,0 +1,29 @@ +module AuthorisedSlugs + extend ActiveSupport::Concern + + included do + before_action :authorise_slug! + end + + def authorise_slug! + if page_sequence.requires_authorisation?(current_slug) && !authorised? + redirect_to( + page_sequence.unauthorised_path( + current_slug, + authorisation.failure_reason + ) + ) + end + end + + def authorised? + authorisation.failure_reason.nil? + end + + def authorisation + journey::Authorisation.new( + answers: journey_session.answers, + slug: current_slug + ) + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 7d1ad597cd..35145faabb 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -6,21 +6,14 @@ class OmniauthCallbacksController < ApplicationController def callback auth = request.env["omniauth.auth"] - # Only keep the attributes permitted by the form - teacher_id_user_info_attributes = auth.extra.raw_info.to_h.slice( - *SignInOrContinueForm::TeacherIdUserInfoForm::DFE_IDENTITY_ATTRIBUTES.map(&:to_s) - ) - - redirect_to( - claim_path( - journey: current_journey_routing_name, - slug: "sign-in-or-continue", - claim: { - logged_in_with_tid: true, - teacher_id_user_info_attributes: teacher_id_user_info_attributes - } - ) - ) + case params[:journey] + when "further-education-payments-provider" + further_education_payments_provider_callback(auth) + else + # The callback route for student loans and additional payments isn't + # namespaced under a journey + additional_payments_callback(auth) + end end def failure @@ -128,4 +121,38 @@ def omniauth_hash request.env["omniauth.auth"] end end + + def further_education_payments_provider_callback(auth) + Journeys::FurtherEducationPayments::Provider::OmniauthCallbackForm.new( + journey_session: journey_session, + auth: auth + ).save! + + session[:slugs] << "sign-in" + + redirect_to( + claim_path( + journey: current_journey_routing_name, + slug: "verify-claim" + ) + ) + end + + def additional_payments_callback(auth) + # Only keep the attributes permitted by the form + teacher_id_user_info_attributes = auth.extra.raw_info.to_h.slice( + *SignInOrContinueForm::TeacherIdUserInfoForm::DFE_IDENTITY_ATTRIBUTES.map(&:to_s) + ) + + redirect_to( + claim_path( + journey: current_journey_routing_name, + slug: "sign-in-or-continue", + claim: { + logged_in_with_tid: true, + teacher_id_user_info_attributes: teacher_id_user_info_attributes + } + ) + ) + end end diff --git a/app/forms/journeys/further_education_payments/provider/claim_submission_form.rb b/app/forms/journeys/further_education_payments/provider/claim_submission_form.rb new file mode 100644 index 0000000000..42974dc726 --- /dev/null +++ b/app/forms/journeys/further_education_payments/provider/claim_submission_form.rb @@ -0,0 +1,17 @@ +# Required to get page sequence to think this is a "normal" journey +module Journeys + module FurtherEducationPayments + module Provider + class ClaimSubmissionForm + def initialize(journey_session) + @journey_session = journey_session + end + + # We don't want page sequence to redirect us + def valid? + false + end + end + end + end +end diff --git a/app/forms/journeys/further_education_payments/provider/omniauth_callback_form.rb b/app/forms/journeys/further_education_payments/provider/omniauth_callback_form.rb new file mode 100644 index 0000000000..d433e341df --- /dev/null +++ b/app/forms/journeys/further_education_payments/provider/omniauth_callback_form.rb @@ -0,0 +1,72 @@ +module Journeys + module FurtherEducationPayments + module Provider + class OmniauthCallbackForm + def initialize(journey_session:, auth:) + @journey_session = journey_session + @auth = auth + end + + def save! + journey_session.answers.assign_attributes( + dfe_sign_in_uid: dfe_sign_in_uid, + dfe_sign_in_organisation_id: dfe_sign_in_organisation_id, + dfe_sign_in_organisation_ukprn: dfe_sign_in_organisation_ukprn, + dfe_sign_in_service_access: dfe_sign_in_service_access?, + dfe_sign_in_role_codes: dfe_sign_in_role_codes, + dfe_sign_in_first_name: dfe_sign_in_first_name, + dfe_sign_in_last_name: dfe_sign_in_last_name, + dfe_sign_in_email: dfe_sign_in_email + ) + + journey_session.save! + end + + private + + attr_reader :journey_session, :auth + + def dfe_sign_in_uid + auth["uid"] + end + + def dfe_sign_in_organisation_ukprn + auth.dig("extra", "raw_info", "organisation", "ukprn") + end + + def dfe_sign_in_organisation_id + auth.dig("extra", "raw_info", "organisation", "id") + end + + def dfe_sign_in_service_access? + dfe_sign_in_user.service_access? + end + + def dfe_sign_in_user + @dfe_sign_in_user ||= DfeSignIn::Api::User.new( + organisation_id: dfe_sign_in_organisation_id, + user_id: dfe_sign_in_uid + ) + end + + def dfe_sign_in_role_codes + return [] unless dfe_sign_in_service_access? + + dfe_sign_in_user.role_codes + end + + def dfe_sign_in_first_name + auth.dig("info", "first_name") + end + + def dfe_sign_in_last_name + auth.dig("info", "last_name") + end + + def dfe_sign_in_email + auth.dig("info", "email") + end + end + end + end +end diff --git a/app/forms/journeys/further_education_payments/provider/session_form.rb b/app/forms/journeys/further_education_payments/provider/session_form.rb new file mode 100644 index 0000000000..6396c86954 --- /dev/null +++ b/app/forms/journeys/further_education_payments/provider/session_form.rb @@ -0,0 +1,9 @@ +module Journeys + module FurtherEducationPayments + module Provider + class SessionForm < Journeys::SessionForm + attribute :claim_id, :string + end + end + end +end diff --git a/app/forms/journeys/further_education_payments/provider/verify_claim_form.rb b/app/forms/journeys/further_education_payments/provider/verify_claim_form.rb new file mode 100644 index 0000000000..8e9d21a8ae --- /dev/null +++ b/app/forms/journeys/further_education_payments/provider/verify_claim_form.rb @@ -0,0 +1,156 @@ +module Journeys + module FurtherEducationPayments + module Provider + class VerifyClaimForm < Form + include CoursesHelper + + ASSERTIONS = { + fixed_contract: %i[ + contract_type + teaching_responsibilities + further_education_teaching_start_year + teaching_hours_per_week + hours_teaching_eligible_subjects + subjects_taught + ], + variable_contract: %i[ + contract_type + teaching_responsibilities + further_education_teaching_start_year + taught_at_least_one_term + teaching_hours_per_week + hours_teaching_eligible_subjects + subjects_taught + teaching_hours_per_week_next_term + ] + } + + attribute :assertions_attributes + + attribute :declaration, :boolean + + validates :declaration, acceptance: true + + validate :all_assertions_answered + + validate :claim_not_already_verified + + delegate :claim, to: :answers + + def claim_reference + claim.reference + end + + def claimant_name + claim.full_name + end + + def claimant_date_of_birth + claim.date_of_birth.strftime("%-d %B %Y") + end + + def claimant_trn + claim.eligibility.teacher_reference_number + end + + def claim_date + claim.created_at.to_date.strftime("%-d %B %Y") + end + + def course_descriptions + @course_descriptions ||= claim.eligibility.courses_taught.map(&:description) + end + + def assertions + @assertions ||= ASSERTIONS.fetch(contract_type).map do |assertion_name| + AssertionForm.new(name: assertion_name) + end + end + + def assertions_attributes=(params) + (params || {}).each do |_, assertion_params| + assertions + .detect { |a| a.name == assertion_params[:name] } + &.assign_attributes(assertion_params) + end + end + + def save + return false unless valid? + + claim.eligibility.update!( + verification: { + assertions: assertions.map(&:attributes), + verifier: { + dfe_sign_in_uid: answers.dfe_sign_in_uid, + first_name: answers.dfe_sign_in_first_name, + last_name: answers.dfe_sign_in_last_name, + email: answers.dfe_sign_in_email + }, + created_at: DateTime.now + } + ) + + claim.save! + + true + end + + def contract_type + if claim.eligibility.fixed_contract? + :fixed_contract + else + :variable_contract + end + end + + private + + def permitted_attributes + super + [assertions_attributes: AssertionForm.attribute_names] + end + + # Make sure the errors in the summary link to the correct nested field + def all_assertions_answered + assertions.each(&:validate).each_with_index do |assertion, i| + assertion.errors.each do |error| + errors.add( + "assertions_attributes[#{i}][#{error.attribute}]", + error.full_message + ) + end + end + end + + def claim_not_already_verified + if claim.eligibility.verified? + errors.add(:base, "Claim has already been verified") + end + end + + class AssertionForm + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :name, :string + attribute :outcome, :boolean + + validates :name, presence: true + validates :outcome, inclusion: { + in: [true, false], + message: "Select an option" + } + + def radio_options + [ + RadioOption.new(id: true, name: "Yes"), + RadioOption.new(id: false, name: "No") + ] + end + + class RadioOption < Struct.new(:id, :name, keyword_init: true); end + end + end + end + end +end diff --git a/app/models/journeys.rb b/app/models/journeys.rb index 1e8159125d..14f48c25ae 100644 --- a/app/models/journeys.rb +++ b/app/models/journeys.rb @@ -10,6 +10,7 @@ def self.table_name_prefix TeacherStudentLoanReimbursement, GetATeacherRelocationPayment, FurtherEducationPayments, + FurtherEducationPayments::Provider, EarlyYearsPayment::Provider::Start, EarlyYearsPayment::Provider::Authenticated ].freeze diff --git a/app/models/journeys/further_education_payments/provider.rb b/app/models/journeys/further_education_payments/provider.rb new file mode 100644 index 0000000000..d6d79b4c10 --- /dev/null +++ b/app/models/journeys/further_education_payments/provider.rb @@ -0,0 +1,32 @@ +module Journeys + module FurtherEducationPayments + module Provider + extend Base + extend self + + ROUTING_NAME = "further-education-payments-provider" + VIEW_PATH = "further_education_payments/provider" + I18N_NAMESPACE = "further_education_payments_provider" + + POLICIES = [] + + FORMS = { + "claims" => { + "verify-claim" => VerifyClaimForm + } + } + + CLAIM_VERIFIER_DFE_SIGN_IN_ROLE_CODE = "teacher_payments_claim_verifier" + + START_WITH_MAGIC_LINK = true + + def self.request_service_access_url(session) + [ + "https://services.signin.education.gov.uk", + "request-service", DfeSignIn.configuration.client_id, + "users", session.answers.dfe_sign_in_uid + ].join("/") + end + end + end +end diff --git a/app/models/journeys/further_education_payments/provider/authorisation.rb b/app/models/journeys/further_education_payments/provider/authorisation.rb new file mode 100644 index 0000000000..6d53442c8f --- /dev/null +++ b/app/models/journeys/further_education_payments/provider/authorisation.rb @@ -0,0 +1,54 @@ +module Journeys + module FurtherEducationPayments + module Provider + class Authorisation + def initialize(answers:, slug:) + @answers = answers + @slug = slug + end + + def failure_reason + return :organisation_mismatch if organisation_mismatch? + return :no_service_access unless answers.dfe_sign_in_service_access? + return :claim_admin if claim_admin? + return :incorrect_role unless role_permits_access? + return :already_verified if already_verified? && slug != "complete" + + nil + end + + private + + attr_reader :answers, :slug + + def organisation_mismatch? + answers.claim.school.ukprn != answers.dfe_sign_in_organisation_ukprn + end + + def role_permits_access? + answers.dfe_sign_in_role_codes.include?( + CLAIM_VERIFIER_DFE_SIGN_IN_ROLE_CODE + ) + end + + def claim_admin? + claim_admin_roles.any? do |role_code| + answers.dfe_sign_in_role_codes.include?(role_code) + end + end + + def claim_admin_roles + [ + DfeSignIn::User::SERVICE_OPERATOR_DFE_SIGN_IN_ROLE_CODE, + DfeSignIn::User::SUPPORT_AGENT_DFE_SIGN_IN_ROLE_CODE, + DfeSignIn::User::PAYROLL_OPERATOR_DFE_SIGN_IN_ROLE_CODE + ] + end + + def already_verified? + answers.claim.eligibility.verified? + end + end + end + end +end diff --git a/app/models/journeys/further_education_payments/provider/eligibility_checker.rb b/app/models/journeys/further_education_payments/provider/eligibility_checker.rb new file mode 100644 index 0000000000..9b0ceb9dd1 --- /dev/null +++ b/app/models/journeys/further_education_payments/provider/eligibility_checker.rb @@ -0,0 +1,12 @@ +# Required to get page sequence to think this is a "normal" journey +module Journeys + module FurtherEducationPayments + module Provider + class EligibilityChecker < Journeys::EligibilityChecker + def ineligible? + false + end + end + end + end +end diff --git a/app/models/journeys/further_education_payments/provider/session.rb b/app/models/journeys/further_education_payments/provider/session.rb new file mode 100644 index 0000000000..88ed584e20 --- /dev/null +++ b/app/models/journeys/further_education_payments/provider/session.rb @@ -0,0 +1,9 @@ +module Journeys + module FurtherEducationPayments + module Provider + class Session < Journeys::Session + attribute :answers, SessionAnswersType.new + end + end + end +end diff --git a/app/models/journeys/further_education_payments/provider/session_answers.rb b/app/models/journeys/further_education_payments/provider/session_answers.rb new file mode 100644 index 0000000000..cdde91c7c1 --- /dev/null +++ b/app/models/journeys/further_education_payments/provider/session_answers.rb @@ -0,0 +1,26 @@ +module Journeys + module FurtherEducationPayments + module Provider + class SessionAnswers < Journeys::SessionAnswers + attribute :claim_id, :string + attribute :declaration, :boolean + attribute :dfe_sign_in_uid, :string + attribute :dfe_sign_in_organisation_id, :string + attribute :dfe_sign_in_organisation_ukprn, :string + attribute :dfe_sign_in_service_access, :boolean, default: false + attribute :dfe_sign_in_role_codes, default: [] + attribute :dfe_sign_in_first_name, :string + attribute :dfe_sign_in_last_name, :string + attribute :dfe_sign_in_email, :string + + def claim + @claim ||= Claim.includes(eligibility: :school).find(claim_id) + end + + def dfe_sign_in_service_access? + !!dfe_sign_in_service_access + end + end + end + end +end diff --git a/app/models/journeys/further_education_payments/provider/session_answers_type.rb b/app/models/journeys/further_education_payments/provider/session_answers_type.rb new file mode 100644 index 0000000000..6fe76c2889 --- /dev/null +++ b/app/models/journeys/further_education_payments/provider/session_answers_type.rb @@ -0,0 +1,7 @@ +module Journeys + module FurtherEducationPayments + module Provider + class SessionAnswersType < ::Journeys::SessionAnswersType; end + end + end +end diff --git a/app/models/journeys/further_education_payments/provider/slug_sequence.rb b/app/models/journeys/further_education_payments/provider/slug_sequence.rb new file mode 100644 index 0000000000..a4c90fa449 --- /dev/null +++ b/app/models/journeys/further_education_payments/provider/slug_sequence.rb @@ -0,0 +1,54 @@ +module Journeys + module FurtherEducationPayments + module Provider + class SlugSequence + SLUGS = [ + "sign-in", + "verify-claim", + "complete", + "unauthorised" + ] + + RESTRICTED_SLUGS = [ + "verify-claim", + "complete" + ] + + def self.verify_claim_url(claim) + Rails.application.routes.url_helpers.new_claim_path( + module_parent::ROUTING_NAME, + answers: { + claim_id: claim.id + } + ) + end + + def self.start_page_url + Rails.application.routes.url_helpers.landing_page_path( + "further-education-payments-provider" + ) + end + + def initialize(journey_session) + @journey_session = journey_session + end + + def slugs + SLUGS + end + + def requires_authorisation?(slug) + RESTRICTED_SLUGS.include?(slug) + end + + def unauthorised_path(slug, failure_reason) + Rails.application.routes.url_helpers.claim_path( + self.class.module_parent::ROUTING_NAME, + "unauthorised", + failure_reason: failure_reason + ) + end + end + end + end +end diff --git a/app/models/journeys/page_sequence.rb b/app/models/journeys/page_sequence.rb index e7633f9e5f..66f0f01e65 100644 --- a/app/models/journeys/page_sequence.rb +++ b/app/models/journeys/page_sequence.rb @@ -5,7 +5,7 @@ module Journeys class PageSequence attr_reader :current_slug - DEAD_END_SLUGS = %w[complete existing-session eligible-later future-eligibility ineligible check-your-email] + DEAD_END_SLUGS = %w[complete existing-session eligible-later future-eligibility ineligible check-your-email unauthorised] OPTIONAL_SLUGS = %w[postcode-search select-home-address reset-claim] def initialize(slug_sequence, completed_slugs, current_slug, journey_session) @@ -61,6 +61,14 @@ def next_required_slug (slugs - completed_slugs - OPTIONAL_SLUGS).first end + def requires_authorisation?(slug) + @slug_sequence.try(:requires_authorisation?, slug) || false + end + + def unauthorised_path(slug, failure_reason) + @slug_sequence.unauthorised_path(slug, failure_reason) + end + private delegate :answers, to: :@journey_session diff --git a/app/models/policies/further_education_payments/eligibility.rb b/app/models/policies/further_education_payments/eligibility.rb index d3aa06bf1f..233d9ce9c9 100644 --- a/app/models/policies/further_education_payments/eligibility.rb +++ b/app/models/policies/further_education_payments/eligibility.rb @@ -3,6 +3,21 @@ module FurtherEducationPayments class Eligibility < ApplicationRecord self.table_name = "further_education_payments_eligibilities" + class Course < Struct.new(:subject, :name, keyword_init: true) + include Journeys::FurtherEducationPayments::CoursesHelper + + def taught? + name != "none" + end + + def description + I18n.t( + "further_education_payments.forms.#{subject}_courses.options.#{name}", + link: link_for_course("#{subject}_courses", name) + ) + end + end + has_one :claim, as: :eligibility, inverse_of: :eligibility belongs_to :possible_school, optional: true, class_name: "School" @@ -18,6 +33,26 @@ def policy def ineligible? false end + + def courses_taught + courses.select(&:taught?) + end + + def courses + subjects_taught.map do |subject| + public_send(:"#{subject}_courses").map do |course| + Course.new(subject: subject, name: course) + end + end.flatten + end + + def fixed_contract? + contract_type != "variable_hours" + end + + def verified? + verification.present? + end end end end diff --git a/app/views/further_education_payments/provider/claims/_unauthorised_already_verified.html.erb b/app/views/further_education_payments/provider/claims/_unauthorised_already_verified.html.erb new file mode 100644 index 0000000000..f0daa2eec6 --- /dev/null +++ b/app/views/further_education_payments/provider/claims/_unauthorised_already_verified.html.erb @@ -0,0 +1,21 @@ +

+ This claim has already been verified +

+ +

+ Email <%= govuk_mail_to("FE-Levellingup.PremiumPayments@education.gov.uk") %> + if you need to: +

+ +<%= govuk_list( + [ + "view the verification form", + "make changes to the details provided in the verification form" + ], + type: :bullet +) %> + +

+ Make sure you include the claim reference number in your email so we can find + the claim and process any changes promptly. +

diff --git a/app/views/further_education_payments/provider/claims/_unauthorised_claim_admin.html.erb b/app/views/further_education_payments/provider/claims/_unauthorised_claim_admin.html.erb new file mode 100644 index 0000000000..8308dce1c4 --- /dev/null +++ b/app/views/further_education_payments/provider/claims/_unauthorised_claim_admin.html.erb @@ -0,0 +1,22 @@ +

+ You do not have access to verify claims for this organisation +

+ +

+ DfE staff do not have access to verify retention payments for further + education teachers. +

+ +

+ If you need to view a claim: +

+ +<%= govuk_list( + [ + "Log in to DfE Sign-in using the ‘Manage Teacher Payments’ organisation.", + "Go to the ‘Search’ tab.", + "Search for the claim using the claim reference number." + ], + type: :number +) %> + diff --git a/app/views/further_education_payments/provider/claims/_unauthorised_incorrect_role.html.erb b/app/views/further_education_payments/provider/claims/_unauthorised_incorrect_role.html.erb new file mode 100644 index 0000000000..b81de7154f --- /dev/null +++ b/app/views/further_education_payments/provider/claims/_unauthorised_incorrect_role.html.erb @@ -0,0 +1,15 @@ +

+ You do not have access to verify claims for this organisation +

+ +

+ If you think you should have access, you need to: +

+ +<%= govuk_list( + [ + "check you have logged in to DfE Sign-in using the correct organisation", + "contact an approver at your organisation to confirm your access rights" + ], + type: :bullet +) %> diff --git a/app/views/further_education_payments/provider/claims/_unauthorised_no_service_access.html.erb b/app/views/further_education_payments/provider/claims/_unauthorised_no_service_access.html.erb new file mode 100644 index 0000000000..28942b8179 --- /dev/null +++ b/app/views/further_education_payments/provider/claims/_unauthorised_no_service_access.html.erb @@ -0,0 +1,35 @@ +

+ You do not have access to this service +

+ +

+ When a claimant applies for a retention payment, an access link will be + sent to your organisation's nominated email address. +

+ +

+You can <%= govuk_link_to( + "request access", + Journeys::FurtherEducationPayments::Provider.request_service_access_url(journey_session) +) %> to verify retention payments for further + education teachers using DfE Sign-in. +

+ +

+ Your request will be sent to all approvers at your organisation. Most + requests will be approved or rejected within 5 days of being raised. +

+ +

+ If you have waited 5 days or longer and you still haven't had a response + to your request: +

+ +<%= govuk_list( + [ + "Log in to DfE Sign-in and go to the 'Organisations' tab.", + "Find the email address of an approver at your organisation.", + "Send a chaser email." + ], + type: :bullet +) %> diff --git a/app/views/further_education_payments/provider/claims/_unauthorised_organisation_mismatch.html.erb b/app/views/further_education_payments/provider/claims/_unauthorised_organisation_mismatch.html.erb new file mode 100644 index 0000000000..5779c1faa0 --- /dev/null +++ b/app/views/further_education_payments/provider/claims/_unauthorised_organisation_mismatch.html.erb @@ -0,0 +1,18 @@ +

+ You cannot verify this claim +

+ +

+ The organisation you have used to log in to DfE Sign-in does not match + the organisation in the claim. +

+ +

+ If your DfE Sign-in account is linked to multiple organisations, check + that you have logged in using the correct one. +

+ +

+ Email <%= govuk_mail_to("FE-Levellingup.PremiumPayments@education.gov.uk") %> + if you have logged in with the correct organisation and need support. +

diff --git a/app/views/further_education_payments/provider/claims/complete.html.erb b/app/views/further_education_payments/provider/claims/complete.html.erb new file mode 100644 index 0000000000..8bef8ed775 --- /dev/null +++ b/app/views/further_education_payments/provider/claims/complete.html.erb @@ -0,0 +1,44 @@ +<% content_for( + :page_title, + page_title( + "Verification complete", + journey: current_journey_routing_name + ) +) %> + +
+
+ +
+

+ Verification complete +

+ +
+ Claim reference number
+ <%= journey_session.answers.claim.reference %> +
+
+ +

+ We have sent you a confirmation email. +

+ +

+ You should keep the confirmation email on file for [recommended time] for + future reference. +

+ +

What happens next

+ +

+ We’ve sent your response to the Further education levelling up premium + payments team. +

+ +

+ They may contact you again to ask for further information. +

+
+
+ diff --git a/app/views/further_education_payments/provider/claims/sign_in.html.erb b/app/views/further_education_payments/provider/claims/sign_in.html.erb new file mode 100644 index 0000000000..0c53a9e0a2 --- /dev/null +++ b/app/views/further_education_payments/provider/claims/sign_in.html.erb @@ -0,0 +1,37 @@ +<% content_for( + :page_title, + page_title( + "Verify a further education retention payment", + journey: current_journey_routing_name + ) +) %> +
+
+

+ Verify a further education retention payment +

+ +

+ Use this service to verify a retention payment for further education + teachers. +

+ +

+ You will need to log in using DfE Sign-in. If you don't have a DfE Sign-in + account yet, we will help you create one. +

+ + <%= button_to( + "/further-education-payments-provider/auth/dfe_fe_provider", + class: "govuk-button govuk-button--start", + data: { + module: "govuk-button" + } + ) do %> + Start now + + <% end %> +
+
diff --git a/app/views/further_education_payments/provider/claims/unauthorised.html.erb b/app/views/further_education_payments/provider/claims/unauthorised.html.erb new file mode 100644 index 0000000000..83aba72ed3 --- /dev/null +++ b/app/views/further_education_payments/provider/claims/unauthorised.html.erb @@ -0,0 +1,24 @@ +<% content_for( + :page_title, + page_title( + "Unauthorised", + journey: current_journey_routing_name + ) +) %> + +
+
+ <% case params[:failure_reason] %> + <% when "organisation_mismatch" %> + <%= render "unauthorised_organisation_mismatch" %> + <% when "no_service_access" %> + <%= render "unauthorised_no_service_access" %> + <% when "incorrect_role" %> + <%= render "unauthorised_incorrect_role" %> + <% when "claim_admin" %> + <%= render "unauthorised_claim_admin" %> + <% when "already_verified" %> + <%= render "unauthorised_already_verified" %> + <% end %> +
+
diff --git a/app/views/further_education_payments/provider/claims/verify_claim.html.erb b/app/views/further_education_payments/provider/claims/verify_claim.html.erb new file mode 100644 index 0000000000..0902792c1d --- /dev/null +++ b/app/views/further_education_payments/provider/claims/verify_claim.html.erb @@ -0,0 +1,104 @@ +<% content_for( + :page_title, + page_title( + @form.t(:title), + journey: current_journey_routing_name, + show_error: @form.errors.any? + ) +) %> + +
+
+

<%= @form.t(:title) %>

+ + <%= govuk_summary_list do |summary_list| %> + <%= summary_list.with_row do |row| %> + <%= row.with_key(text: @form.t(:claim_reference)) %> + <%= row.with_value(text: @form.claim_reference) %> + <% end %> + + <%= summary_list.with_row do |row| %> + <%= row.with_key(text: @form.t(:claimant_name)) %> + <%= row.with_value(text: @form.claimant_name) %> + <% end %> + + <%= summary_list.with_row do |row| %> + <%= row.with_key(text: @form.t(:claimant_date_of_birth)) %> + <%= row.with_value(text: @form.claimant_date_of_birth) %> + <% end %> + + <%= summary_list.with_row do |row| %> + <%= row.with_key(text: @form.t(:claimant_trn)) %> + <%= row.with_value(text: @form.claimant_trn) %> + <% end %> + + <%= summary_list.with_row do |row| %> + <%= row.with_key(text: @form.t(:claim_date)) %> + <%= row.with_value(text: @form.claim_date) %> + <% end %> + <% end %> + +

+ We need you to verify some information +

+ + <%= form_with( + model: @form, + url: claim_path(current_journey_routing_name), + builder: GOVUKDesignSystemFormBuilder::FormBuilder, + html: { novalidate: false } + ) do |f| %> + <%= f.govuk_error_summary %> + + <%= f.fields_for :assertions do |ff| %> + <%= ff.govuk_collection_radio_buttons( + :outcome, + ff.object.radio_options, + :id, + :name, + nil, + legend: { + size: "s", + text: f.object.t( + [:assertions, f.object.contract_type, ff.object.name], + claimant: f.object.claimant_name, + provider: f.object.claim.school.name, + ), + }, + hint: -> do + if ff.object.name.to_s == "subjects_taught" + govuk_list( + f.object.course_descriptions.map(&:html_safe), + type: :bullet + ) + end + end + ) %> + + <%= ff.hidden_field :name %> + <% end %> + + <%= f.govuk_check_boxes_fieldset( + :declaration, + multiple: false, + legend: { + text: f.object.t(:declaration_title), + size: "m" + } + ) do %> + <%= f.govuk_check_box( + :declaration, + "1", + "0", + multiple: false, + link_errors: true, + label: { + text: f.object.t(:declaration) + } + ) %> + <% end %> + + <%= f.govuk_submit "Submit" %> + <% end %> +
+
diff --git a/app/views/further_education_payments/provider/landing_page.html.erb b/app/views/further_education_payments/provider/landing_page.html.erb new file mode 100644 index 0000000000..0a08c78564 --- /dev/null +++ b/app/views/further_education_payments/provider/landing_page.html.erb @@ -0,0 +1,17 @@ +
+
+

+ You cannot access this service from DfE Sign-in +

+ +

+ When a claimant applies for a retention payment, an access link will be + sent to your organisation's nominated email address. +

+ +

+ If you have any questions or need support, email: + <%= govuk_mail_to("FE-Levellingup.PremiumPayments@education.gov.uk") %> +

+
+
diff --git a/config/analytics_blocklist.yml b/config/analytics_blocklist.yml index 2711fdefac..ff3998d33f 100644 --- a/config/analytics_blocklist.yml +++ b/config/analytics_blocklist.yml @@ -121,5 +121,6 @@ - teacher_reference_number :further_education_payments_eligibilities: - teacher_reference_number + - verification :journeys_sessions: - answers diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 53388abd79..e15de40f9e 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -11,8 +11,11 @@ dfe_sign_in_issuer_uri = ENV["DFE_SIGN_IN_ISSUER"].present? ? URI(ENV["DFE_SIGN_IN_ISSUER"]) : nil +dfe_sign_in_fe_provider_callback_path = "/further-education-payments-provider/auth/callback" + if ENV["DFE_SIGN_IN_REDIRECT_BASE_URL"].present? dfe_sign_in_redirect_uri = URI.join(ENV["DFE_SIGN_IN_REDIRECT_BASE_URL"], "/admin/auth/callback") + dfe_sign_in_fe_provider_redirect_uri = URI.join(ENV["DFE_SIGN_IN_REDIRECT_BASE_URL"], dfe_sign_in_fe_provider_callback_path) end tid_sign_in_endpoint_uri = ENV["TID_SIGN_IN_API_ENDPOINT"].present? ? URI(ENV["TID_SIGN_IN_API_ENDPOINT"]) : nil @@ -70,6 +73,25 @@ def self.bypass? } end + provider :openid_connect, { + name: :dfe_fe_provider, + discovery: true, + response_type: :code, + scope: %i[openid email organisation first_name last_name], + callback_path: dfe_sign_in_fe_provider_callback_path, + path_prefix: "/further-education-payments-provider/auth", + client_options: { + port: dfe_sign_in_issuer_uri&.port, + scheme: dfe_sign_in_issuer_uri&.scheme, + host: dfe_sign_in_issuer_uri&.host, + identifier: ENV["DFE_SIGN_IN_IDENTIFIER"], + secret: ENV["DFE_SIGN_IN_SECRET"], + redirect_uri: dfe_sign_in_fe_provider_redirect_uri&.to_s + }, + issuer: + ("#{dfe_sign_in_issuer_uri}:#{dfe_sign_in_issuer_uri.port}" if dfe_sign_in_issuer_uri.present?) + } + provider :openid_connect, { name: :tid, callback_path: "/claim/auth/tid/callback", diff --git a/config/locales/en.yml b/config/locales/en.yml index 8990ade4c0..46dd8210a0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1094,6 +1094,38 @@ en: By submitting this you are confirming that, to the best of your knowledge, the details you are providing are correct. btn_text: Accept and send + further_education_payments_provider: + journey_name: Claim incentive payments for further education teachers - provider + feedback_email: "FE-Levellingup.PremiumPayments@education.gov.uk" + support_email_address: "FE-Levellingup.PremiumPayments@education.gov.uk" + forms: + verify_claim: + title: "Review a financial incentive payment claim" + declaration_title: "Declaration" + declaration: "To the best of my knowledge, I confirm that the information provided in this form is correct." + claim_reference: "Claim reference" + claimant_name: "Claimant name" + claimant_date_of_birth: "Claimant date of birth" + claimant_trn: "Claimant teacher reference number (TRN)" + claim_date: "Claim date" + assertions: + fixed_contract: + contract_type: "Does %{claimant} have a permanent contract of employment at %{provider}?" + teaching_responsibilities: "Is %{claimant} a member of staff with teaching responsibilities?" + further_education_teaching_start_year: "Is %{claimant} in the first 5 years of their further education teaching career in England?" + teaching_hours_per_week: "Is %{claimant} timetabled to teach an average of 12 hours per week during the current term?" + hours_teaching_eligible_subjects: "For at least half of their timetabled teaching hours, does %{claimant} teach 16- to 19-year-olds, including those up to age 25 with an Education, Health and Care Plan (EHCP)?" + subjects_taught: "For at least half of their timetabled teaching hours, does %{claimant} teach:" + variable_contract: + contract_type: "Does %{claimant} have a variable hour contract of employment at %{provider}?" + teaching_responsibilities: "Is %{claimant} a member of staff with teaching responsibilities?" + further_education_teaching_start_year: "Is %{claimant} in the first 5 years of their further education teaching career in England?" + taught_at_least_one_term: "Has %{claimant} taught for at least one academic term at %{provider}?" + teaching_hours_per_week: "Is %{claimant} timetabled to teach an average of 12 hours per week during the current term?" + hours_teaching_eligible_subjects: "For at least half of their timetabled teaching hours, does %{claimant} teach 16- to 19-year-olds, including those up to age 25 with an Education, Health and Care Plan (EHCP)?" + subjects_taught: "For at least half of their timetabled teaching hours, does %{claimant} teach:" + teaching_hours_per_week_next_term: "Will %{claimant} be timetabled to teach at least 2.5 hours per week next term?" + early_years_payment: claim_description: for an early years financial incentive payment early_years_payment_provider_start: diff --git a/config/routes.rb b/config/routes.rb index 946391b42a..fbbef8bdbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,6 +61,10 @@ def matches?(request) resources :reminders, only: [:show, :update], param: :slug, constraints: {slug: %r{#{Journeys::AdditionalPaymentsForTeaching::SlugSequence::REMINDER_SLUGS.join("|")}}}, controller: "journeys/additional_payments_for_teaching/reminders" end + scope constraints: {journey: "further-education-payments-provider"} do + get "auth/callback", to: "omniauth_callbacks#callback" + end + scope path: "/", constraints: {journey: Regexp.new(Journeys.all_routing_names.join("|"))} do get "landing-page", to: "static_pages#landing_page", as: :landing_page end diff --git a/db/migrate/20240819141523_add_verification_to_further_education_payments_eligibilities.rb b/db/migrate/20240819141523_add_verification_to_further_education_payments_eligibilities.rb new file mode 100644 index 0000000000..b961231495 --- /dev/null +++ b/db/migrate/20240819141523_add_verification_to_further_education_payments_eligibilities.rb @@ -0,0 +1,5 @@ +class AddVerificationToFurtherEducationPaymentsEligibilities < ActiveRecord::Migration[7.0] + def change + add_column :further_education_payments_eligibilities, :verification, :jsonb, default: {} + end +end diff --git a/db/schema.rb b/db/schema.rb index 0e77b03a1f..58012cdeb7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -246,6 +246,7 @@ t.boolean "subject_to_formal_performance_action" t.boolean "subject_to_disciplinary_action" t.boolean "half_teaching_hours" + t.jsonb "verification", default: {} t.index ["possible_school_id"], name: "index_fe_payments_eligibilities_on_possible_school_id" t.index ["school_id"], name: "index_fe_payments_eligibilities_on_school_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 5e2d658d9a..96d65f3a7f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -11,6 +11,7 @@ Journeys::Configuration.create!(routing_name: Journeys::AdditionalPaymentsForTeaching::ROUTING_NAME, current_academic_year: AcademicYear.current) Journeys::Configuration.create!(routing_name: Journeys::GetATeacherRelocationPayment::ROUTING_NAME, current_academic_year: AcademicYear.current) Journeys::Configuration.create!(routing_name: Journeys::FurtherEducationPayments::ROUTING_NAME, current_academic_year: AcademicYear.current) + Journeys::Configuration.create!(routing_name: Journeys::FurtherEducationPayments::Provider::ROUTING_NAME, current_academic_year: AcademicYear.current) Journeys::Configuration.create!(routing_name: Journeys::EarlyYearsPayment::Provider::Start::ROUTING_NAME, current_academic_year: AcademicYear.current) Journeys::Configuration.create!(routing_name: Journeys::EarlyYearsPayment::Provider::Authenticated::ROUTING_NAME, current_academic_year: AcademicYear.current) diff --git a/lib/dfe_sign_in/api/user.rb b/lib/dfe_sign_in/api/user.rb index f7464ef05e..9a11ba8043 100644 --- a/lib/dfe_sign_in/api/user.rb +++ b/lib/dfe_sign_in/api/user.rb @@ -58,20 +58,32 @@ def has_role?(role_code) end def role_codes + unless service_access? + raise ExternalServerError, "#{response.code}: #{response.body}" unless response.code.eql?("200") + end + body["roles"].map { |r| r["code"] } end + def service_access? + response.code == "200" + end + + def service_error? + response.code == "500" + end + private def body - @body ||= get(uri) + @body ||= JSON.parse(response.body) end - def uri - @uri ||= begin + def response + @response ||= begin uri = URI(DfeSignIn.configuration.base_url) uri.path = "/services/#{DfeSignIn.configuration.client_id}/organisations/#{organisation_id}/users/#{user_id}" - uri + dfe_sign_in_request(uri) end end end diff --git a/lib/dfe_sign_in/utils.rb b/lib/dfe_sign_in/utils.rb index b2a123a8a0..879e45d391 100644 --- a/lib/dfe_sign_in/utils.rb +++ b/lib/dfe_sign_in/utils.rb @@ -10,17 +10,21 @@ def generate_jwt_token end def get(uri) + response = dfe_sign_in_request(uri) + + raise ExternalServerError, "#{response.code}: #{response.body}" unless response.code.eql?("200") + + JSON.parse(response.body) + end + + def dfe_sign_in_request(uri) request = Net::HTTP::Get.new(uri) request["Authorization"] = "bearer #{generate_jwt_token}" request["Content-Type"] = "application/json" - response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| + Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } - - raise ExternalServerError, "#{response.code}: #{response.body}" unless response.code.eql?("200") - - JSON.parse(response.body) end end end diff --git a/spec/factories/journey_configurations.rb b/spec/factories/journey_configurations.rb index 57907de655..ca2db29ffb 100644 --- a/spec/factories/journey_configurations.rb +++ b/spec/factories/journey_configurations.rb @@ -30,6 +30,10 @@ routing_name { Journeys::FurtherEducationPayments::ROUTING_NAME } end + trait :further_education_payments_provider do + routing_name { Journeys::FurtherEducationPayments::Provider::ROUTING_NAME } + end + trait :early_years_payment_provider_start do routing_name { Journeys::EarlyYearsPayment::Provider::Start::ROUTING_NAME } end diff --git a/spec/factories/journeys/further_education_payments/provider/answers.rb b/spec/factories/journeys/further_education_payments/provider/answers.rb new file mode 100644 index 0000000000..fb74f11da3 --- /dev/null +++ b/spec/factories/journeys/further_education_payments/provider/answers.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory( + :further_education_payments_provider_answers, + class: "Journeys::FurtherEducationPayments::Provider::SessionAnswers" + ) do + end +end diff --git a/spec/factories/journeys/further_education_payments/provider/session.rb b/spec/factories/journeys/further_education_payments/provider/session.rb new file mode 100644 index 0000000000..cf18f311f9 --- /dev/null +++ b/spec/factories/journeys/further_education_payments/provider/session.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory( + :further_education_payments_provider_session, + class: "Journeys::FurtherEducationPayments::Provider::Session" + ) do + journey { Journeys::FurtherEducationPayments::Provider::ROUTING_NAME } + end +end diff --git a/spec/factories/policies/further_education_payments/eligibilities.rb b/spec/factories/policies/further_education_payments/eligibilities.rb index 5867f6554f..bb1447651d 100644 --- a/spec/factories/policies/further_education_payments/eligibilities.rb +++ b/spec/factories/policies/further_education_payments/eligibilities.rb @@ -1,6 +1,49 @@ FactoryBot.define do factory :further_education_payments_eligibility, class: "Policies::FurtherEducationPayments::Eligibility" do + claim + school { create(:school, :further_education) } + trait :eligible do end + + trait :verified do + verification do + { + "assertions" => [ + { + "name" => "contract_type", + "outcome" => true + }, + { + "name" => "teaching_responsibilities", + "outcome" => true + }, + { + "name" => "further_education_teaching_start_year", + "outcome" => true + }, + { + "name" => "teaching_hours_per_week", + "outcome" => true + }, + { + "name" => "hours_teaching_eligible_subjects", + "outcome" => false + }, + { + "name" => "subjects_taught", + "outcome" => false + } + ], + "verifier" => { + "dfe_sign_in_uid" => "123", + "first_name" => "Seymoure", + "last_name" => "Skinner", + "email" => "seymore.skinner@springfield-elementary.edu" + }, + "created_at" => "2024-01-01T12:00:00.000+00:00" + } + end + end end end diff --git a/spec/features/further_education_payments/provider/provider_verifying_claims_spec.rb b/spec/features/further_education_payments/provider/provider_verifying_claims_spec.rb new file mode 100644 index 0000000000..baef59cae5 --- /dev/null +++ b/spec/features/further_education_payments/provider/provider_verifying_claims_spec.rb @@ -0,0 +1,650 @@ +require "rails_helper" + +RSpec.feature "Provider verifying claims" do + before do + create(:journey_configuration, :further_education_payments_provider) + end + + scenario "provider visits a claim without service access" do + fe_provider = create(:school, :further_education, name: "Springfield A&M") + + claim = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "AB123456", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + claim: claim, + school: fe_provider + ) + + mock_dfe_sign_in_auth_session( + provider: :dfe_fe_provider, + auth_hash: { + uid: "11111", + extra: { + raw_info: { + organisation: { + id: "22222", + ukprn: fe_provider.ukprn + } + } + } + } + ) + + # https://github.com/DFE-Digital/login.dfe.public-api?tab=readme-ov-file#get-user-access-to-service + stub_failed_dfe_sign_in_user_info_request( + "11111", + "22222", + status: 404 + ) + + claim_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim) + + visit claim_link + + click_on "Start now" + + expect(page).to have_text("You do not have access to this service") + + expect(page).to have_text( + "You can request access to verify retention payments for further education teachers using DfE Sign-in." + ) + + # Try to visit the restricted slug directly + visit "/further-education-payments-provider/verify-claim" + + expect(page).to have_text("You do not have access to this service") + + expect(page).to have_text( + "You can request access to verify retention payments for further education teachers using DfE Sign-in." + ) + end + + scenario "provider visits a claim for the wrong organisation" do + fe_provider = create(:school, :further_education, name: "Springfield A&M") + + other_provider = create( + :school, + :further_education, + name: "Springfield Elementary" + ) + + claim = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "AB123456", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + claim: claim, + school: fe_provider + ) + + mock_dfe_sign_in_auth_session( + provider: :dfe_fe_provider, + auth_hash: { + uid: "11111", + extra: { + raw_info: { + organisation: { + id: "22222", + ukprn: other_provider.ukprn + } + } + } + } + ) + + stub_dfe_sign_in_user_info_request( + "11111", + "22222", + "some-role" + ) + + claim_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim) + + visit claim_link + + click_on "Start now" + + expect(page).to have_text( + "The organisation you have used to log in to DfE Sign-in does not match the organisation in the claim." + ) + + # Try to visit the restricted slug directly + visit "/further-education-payments-provider/verify-claim" + + expect(page).to have_text( + "The organisation you have used to log in to DfE Sign-in does not match the organisation in the claim." + ) + end + + scenario "provider visits claim with the wrong role" do + fe_provider = create(:school, :further_education, name: "Springfield A&M") + + claim = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "AB123456", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + claim: claim, + school: fe_provider + ) + + mock_dfe_sign_in_auth_session( + provider: :dfe_fe_provider, + auth_hash: { + uid: "11111", + extra: { + raw_info: { + organisation: { + id: "22222", + ukprn: fe_provider.ukprn + } + } + } + } + ) + + stub_dfe_sign_in_user_info_request( + "11111", + "22222", + "some-other-role" + ) + + claim_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim) + + visit claim_link + + click_on "Start now" + + expect(page).to have_text( + "You do not have access to verify claims for this organisation" + ) + + expect(page).to have_text( + "contact an approver at your organisation to confirm your access rights" + ) + + # Try to visit the restricted slug directly + visit "/further-education-payments-provider/verify-claim" + + expect(page).to have_text( + "You do not have access to verify claims for this organisation" + ) + end + + scenario "admin visits the claim" do + fe_provider = create(:school, :further_education, name: "Springfield A&M") + + claim = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "AB123456", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + claim: claim, + school: fe_provider + ) + + mock_dfe_sign_in_auth_session( + provider: :dfe_fe_provider, + auth_hash: { + uid: "11111", + extra: { + raw_info: { + organisation: { + id: "22222", + ukprn: fe_provider.ukprn + } + } + } + } + ) + + stub_dfe_sign_in_user_info_request( + "11111", + "22222", + [ + DfeSignIn::User::SERVICE_OPERATOR_DFE_SIGN_IN_ROLE_CODE + ] + ) + + claim_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim) + + visit claim_link + + click_on "Start now" + + expect(page).to have_text( + "You do not have access to verify claims for this organisation" + ) + + expect(page).to have_text( + "DfE staff do not have access to verify retention payments for further education teachers." + ) + + # Try to visit the restricted slug directly + visit "/further-education-payments-provider/verify-claim" + + expect(page).to have_text( + "You do not have access to verify claims for this organisation" + ) + end + + scenario "provider visits a claim with an inprogress session" do + fe_provider = create(:school, :further_education, name: "Springfield A and M") + + claim_1 = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "CLAIM1", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + claim: claim_1, + school: fe_provider + ) + + claim_2 = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "CLAIM2", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + claim: claim_2, + school: fe_provider + ) + + mock_dfe_sign_in_auth_session( + provider: :dfe_fe_provider, + auth_hash: { + uid: "11111", + extra: { + raw_info: { + organisation: { + id: "22222", + ukprn: fe_provider.ukprn + } + } + } + } + ) + + stub_dfe_sign_in_user_info_request( + "11111", + "22222", + Journeys::FurtherEducationPayments::Provider::CLAIM_VERIFIER_DFE_SIGN_IN_ROLE_CODE + ) + + claim_1_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim_1) + + claim_2_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim_2) + + visit claim_1_link + + click_on "Start now" + + visit claim_2_link + + click_on "Start now" + + expect(page).to have_text "Review a financial incentive payment claim" + + expect(page).to have_text "Claim referenceCLAIM2" + end + + scenario "provider visits a claim that has already been verified" do + fe_provider = create(:school, :further_education, name: "Springfield A and M") + + claim = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "AB123456", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + :verified, + claim: claim, + school: fe_provider + ) + + mock_dfe_sign_in_auth_session( + provider: :dfe_fe_provider, + auth_hash: { + uid: "11111", + extra: { + raw_info: { + organisation: { + id: "22222", + ukprn: fe_provider.ukprn + } + } + } + } + ) + + stub_dfe_sign_in_user_info_request( + "11111", + "22222", + Journeys::FurtherEducationPayments::Provider::CLAIM_VERIFIER_DFE_SIGN_IN_ROLE_CODE + ) + + claim_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim) + + visit claim_link + + click_on "Start now" + + expect(page).to have_text "This claim has already been verified" + + # Try to visit the restricted slug directly + visit "/further-education-payments-provider/verify-claim" + + expect(page).to have_text "This claim has already been verified" + end + + scenario "provider approves a fixed contract claim" do + fe_provider = create(:school, :further_education, name: "Springfield A and M") + + claim = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "AB123456", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + claim: claim, + school: fe_provider, + contract_type: "fixed_term", + subjects_taught: ["engineering_manufacturing"], + engineering_manufacturing_courses: [ + "approved_level_321_transportation", + "level2_3_apprenticeship" + ] + ) + + mock_dfe_sign_in_auth_session( + provider: :dfe_fe_provider, + auth_hash: { + uid: "11111", + extra: { + raw_info: { + organisation: { + id: "22222", + ukprn: fe_provider.ukprn + } + } + } + } + ) + + stub_dfe_sign_in_user_info_request( + "11111", + "22222", + Journeys::FurtherEducationPayments::Provider::CLAIM_VERIFIER_DFE_SIGN_IN_ROLE_CODE + ) + + claim_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim) + + visit claim_link + + click_on "Start now" + + expect(page).to have_text "Review a financial incentive payment claim" + # The text generated by the dl tag doesn not include a space between the + # label and value (displays as expected in browser). + expect(page).to have_text "Claim referenceAB123456" + expect(page).to have_text "Claimant nameEdna Krabappel" + expect(page).to have_text "Claimant date of birth3 July 1945" + # FIXME RL enable this test once we've added the TRN to the eligibility + # expect(page).to have_text "Claimant teacher reference number (TRN)1234567" + expect(page).to have_text "Claim date1 August 2024" + + within_fieldset( + "Does Edna Krabappel have a permanent contract of employment at " \ + "Springfield A and M?" + ) do + choose "Yes" + end + + within_fieldset( + "Is Edna Krabappel a member of staff with teaching responsibilities?" + ) do + choose "Yes" + end + + within_fieldset( + "Is Edna Krabappel in the first 5 years of their further education " \ + "teaching career in England?" + ) do + choose "Yes" + end + + within_fieldset( + "Is Edna Krabappel timetabled to teach an average of 12 hours per " \ + "week during the current term?" + ) do + choose "Yes" + end + + within_fieldset( + "For at least half of their timetabled teaching hours, does " \ + "Edna Krabappel teach 16- to 19-year-olds, including those up to " \ + "age 25 with an Education, Health and Care Plan (EHCP)?" + ) do + choose "Yes" + end + + expect(page).to have_text( + "Qualifications approved for funding at level 3 and below in the " \ + "transportation operations and maintenance (opens in new tab) sector " \ + "subject area" + ) + + expect(page).to have_text( + "Level 2 or level 3 apprenticeships in the engineering and " \ + "manufacturing occupational route (opens in new tab)" + ) + + within_fieldset( + "For at least half of their timetabled teaching hours, does " \ + "Edna Krabappel teach:" + ) do + choose "Yes" + end + + check "To the best of my knowledge, I confirm that the information provided in this form is correct." + + click_on "Submit" + + expect(page).to have_content "Verification complete" + expect(page).to have_text "Claim reference number AB123456" + end + + scenario "provider approves a variable contract claim" do + fe_provider = create(:school, :further_education, name: "Springfield A and M") + + claim = create( + :claim, + first_name: "Edna", + surname: "Krabappel", + date_of_birth: Date.new(1945, 7, 3), + reference: "AB123456", + created_at: DateTime.new(2024, 8, 1, 9, 0, 0) + ) + + create( + :further_education_payments_eligibility, + claim: claim, + school: fe_provider, + contract_type: "variable_hours", + subjects_taught: ["engineering_manufacturing"], + engineering_manufacturing_courses: [ + "approved_level_321_transportation", + "level2_3_apprenticeship" + ] + ) + + mock_dfe_sign_in_auth_session( + provider: :dfe_fe_provider, + auth_hash: { + uid: "11111", + extra: { + raw_info: { + organisation: { + id: "22222", + ukprn: fe_provider.ukprn + } + } + } + } + ) + + stub_dfe_sign_in_user_info_request( + "11111", + "22222", + Journeys::FurtherEducationPayments::Provider::CLAIM_VERIFIER_DFE_SIGN_IN_ROLE_CODE + ) + + claim_link = Journeys::FurtherEducationPayments::Provider::SlugSequence.verify_claim_url(claim) + + visit claim_link + + click_on "Start now" + + expect(page).to have_text "Review a financial incentive payment claim" + # The text generated by the dl tag doesn not include a space between the + # label and value (displays as expected in browser). + expect(page).to have_text "Claim referenceAB123456" + expect(page).to have_text "Claimant nameEdna Krabappel" + expect(page).to have_text "Claimant date of birth3 July 1945" + # FIXME RL enable this test once we've added the TRN to the eligibility + # expect(page).to have_text "Claimant teacher reference number (TRN)1234567" + expect(page).to have_text "Claim date1 August 2024" + + within_fieldset( + "Does Edna Krabappel have a variable hour contract of employment at " \ + "Springfield A and M?" + ) do + choose "Yes" + end + + within_fieldset( + "Is Edna Krabappel a member of staff with teaching responsibilities?" + ) do + choose "Yes" + end + + within_fieldset( + "Is Edna Krabappel in the first 5 years of their further education " \ + "teaching career in England?" + ) do + choose "Yes" + end + + within_fieldset( + "Has Edna Krabappel taught for at least one academic term at " \ + "Springfield A and M?" + ) do + choose "Yes" + end + + within_fieldset( + "Is Edna Krabappel timetabled to teach an average of 12 hours per " \ + "week during the current term?" + ) do + choose "Yes" + end + + within_fieldset( + "For at least half of their timetabled teaching hours, does " \ + "Edna Krabappel teach 16- to 19-year-olds, including those up to " \ + "age 25 with an Education, Health and Care Plan (EHCP)?" + ) do + choose "Yes" + end + + expect(page).to have_text( + "Qualifications approved for funding at level 3 and below in the " \ + "transportation operations and maintenance (opens in new tab) sector " \ + "subject area" + ) + + expect(page).to have_text( + "Level 2 or level 3 apprenticeships in the engineering and " \ + "manufacturing occupational route (opens in new tab)" + ) + + within_fieldset( + "For at least half of their timetabled teaching hours, does " \ + "Edna Krabappel teach:" + ) do + choose "Yes" + end + + within_fieldset( + "Will Edna Krabappel be timetabled to teach at least 2.5 hours per " \ + "week next term?" + ) do + choose "Yes" + end + + check( + "To the best of my knowledge, I confirm that the information " \ + "provided in this form is correct." + ) + + click_on "Submit" + + expect(page).to have_content "Verification complete" + expect(page).to have_text "Claim reference number AB123456" + end + + scenario "provider visits the landing page" do + visit( + Journeys::FurtherEducationPayments::Provider::SlugSequence.start_page_url + ) + + expect(page).to have_text "You cannot access this service from DfE Sign-in" + end +end diff --git a/spec/forms/journeys/further_education_payments/provider/omniauth_callback_form_spec.rb b/spec/forms/journeys/further_education_payments/provider/omniauth_callback_form_spec.rb new file mode 100644 index 0000000000..3c5b263959 --- /dev/null +++ b/spec/forms/journeys/further_education_payments/provider/omniauth_callback_form_spec.rb @@ -0,0 +1,119 @@ +require "rails_helper" + +RSpec.describe Journeys::FurtherEducationPayments::Provider::OmniauthCallbackForm do + let(:journey_session) do + create(:further_education_payments_provider_session) + end + + let(:form) do + described_class.new( + journey_session: journey_session, + auth: auth + ) + end + + describe "#save!" do + before do + allow(DfeSignIn::Api::User).to receive(:new).and_return(dfe_sign_in_user) + end + + let(:dfe_sign_in_user) do + instance_double( + DfeSignIn::Api::User, + service_access?: service_access, + role_codes: ["teacher_payments_claim_verifier"] + ) + end + + let(:auth) do + OmniAuth::AuthHash.new( + "uid" => "11111", + "info" => { + "email" => "seymore.skinner@springfield-elementary.edu", + "first_name" => "Seymoure", + "last_name" => "Skinner" + }, + "extra" => { + "raw_info" => { + "organisation" => { + "id" => "22222", + "ukprn" => "12345678" + } + } + } + ) + end + + context "with access to the service" do + let(:service_access) { true } + + it "updates the session with the auth details from dfe signin" do + expect { form.save! }.to( + change(journey_session.answers, :dfe_sign_in_uid).from(nil).to("11111").and( + change(journey_session.answers, :dfe_sign_in_organisation_ukprn) + .from(nil) + .to("12345678") + ).and( + change(journey_session.answers, :dfe_sign_in_organisation_id) + .from(nil) + .to("22222") + ).and( + change(journey_session.answers, :dfe_sign_in_service_access?) + .from(false) + .to(true) + ).and( + change(journey_session.answers, :dfe_sign_in_role_codes) + .from([]) + .to(["teacher_payments_claim_verifier"]) + ).and( + change(journey_session.answers, :dfe_sign_in_first_name) + .from(nil) + .to("Seymoure") + ).and( + change(journey_session.answers, :dfe_sign_in_last_name) + .from(nil) + .to("Skinner") + ).and( + change(journey_session.answers, :dfe_sign_in_email) + .from(nil) + .to("seymore.skinner@springfield-elementary.edu") + ) + ) + end + end + + context "without access to the service" do + let(:service_access) { false } + + it "sets the service access flag to false" do + expect { form.save! }.to( + change(journey_session.answers, :dfe_sign_in_uid).from(nil).to("11111").and( + change(journey_session.answers, :dfe_sign_in_organisation_ukprn) + .from(nil) + .to("12345678") + ).and( + change(journey_session.answers, :dfe_sign_in_organisation_id) + .from(nil) + .to("22222") + ).and( + not_change(journey_session.answers, :dfe_sign_in_service_access?) + ).and( + not_change(journey_session.answers, :dfe_sign_in_role_codes) + ).and( + change(journey_session.answers, :dfe_sign_in_first_name) + .from(nil) + .to("Seymoure") + ).and( + change(journey_session.answers, :dfe_sign_in_last_name) + .from(nil) + .to("Skinner") + ).and( + change(journey_session.answers, :dfe_sign_in_email) + .from(nil) + .to("seymore.skinner@springfield-elementary.edu") + ) + ) + end + end + end +end diff --git a/spec/forms/journeys/further_education_payments/provider/verify_claim_form_spec.rb b/spec/forms/journeys/further_education_payments/provider/verify_claim_form_spec.rb new file mode 100644 index 0000000000..835c20c6c1 --- /dev/null +++ b/spec/forms/journeys/further_education_payments/provider/verify_claim_form_spec.rb @@ -0,0 +1,176 @@ +require "rails_helper" + +RSpec.describe Journeys::FurtherEducationPayments::Provider::VerifyClaimForm, type: :model do + let(:journey) { Journeys::FurtherEducationPayments::Provider } + + let(:eligibility) { create(:further_education_payments_eligibility) } + + let(:claim) { eligibility.claim } + + let(:journey_session) do + create( + :further_education_payments_provider_session, + answers: { + claim_id: claim.id, + dfe_sign_in_uid: "123", + dfe_sign_in_first_name: "Seymoure", + dfe_sign_in_last_name: "Skinner", + dfe_sign_in_email: "seymore.skinner@springfield-elementary.edu" + } + ) + end + + let(:params) { ActionController::Parameters.new } + + let(:form) do + described_class.new( + journey: journey, + journey_session: journey_session, + params: params + ) + end + + describe "validations" do + subject { form } + + it do + is_expected.to validate_acceptance_of(:declaration) + end + + it "validates all assertions are answered" do + form.validate + + form.assertions.each do |assertion| + expect(assertion.errors[:outcome]).to eq(["Select an option"]) + end + end + + it "validates the claim hasn't already been verified" do + eligibility.update!( + verification: { + assertions: [ + {name: "contract_type", outcome: true} + ] + } + ) + + form.validate + + expect(form.errors[:base]).to eq(["Claim has already been verified"]) + end + end + + describe "#assertions" do + subject { form.assertions.map(&:name) } + + context "with a fixed term contract" do + let(:eligibility) do + create( + :further_education_payments_eligibility, + contract_type: "permanent" + ) + end + + it do + is_expected.to eq( + %w[ + contract_type + teaching_responsibilities + further_education_teaching_start_year + teaching_hours_per_week + hours_teaching_eligible_subjects + subjects_taught + ] + ) + end + end + + context "with a variable hours contract" do + let(:eligibility) do + create( + :further_education_payments_eligibility, + contract_type: "variable_hours" + ) + end + + it do + is_expected.to eq( + %w[ + contract_type + teaching_responsibilities + further_education_teaching_start_year + taught_at_least_one_term + teaching_hours_per_week + hours_teaching_eligible_subjects + subjects_taught + teaching_hours_per_week_next_term + ] + ) + end + end + end + + describe "#save" do + let(:params) do + ActionController::Parameters.new( + { + claim: { + declaration: "1", + assertions_attributes: { + "0": {name: "contract_type", outcome: "1"}, + "1": {name: "teaching_responsibilities", outcome: "1"}, + "2": {name: "further_education_teaching_start_year", outcome: "1"}, + "3": {name: "teaching_hours_per_week", outcome: "1"}, + "4": {name: "hours_teaching_eligible_subjects", outcome: "0"}, + "5": {name: "subjects_taught", outcome: "0"} + } + } + } + ) + end + + it "verifies the claim" do + travel_to DateTime.new(2024, 1, 1, 12, 0, 0) do + form.save + end + + expect(claim.reload.eligibility.verification).to match( + { + "assertions" => [ + { + "name" => "contract_type", + "outcome" => true + }, + { + "name" => "teaching_responsibilities", + "outcome" => true + }, + { + "name" => "further_education_teaching_start_year", + "outcome" => true + }, + { + "name" => "teaching_hours_per_week", + "outcome" => true + }, + { + "name" => "hours_teaching_eligible_subjects", + "outcome" => false + }, + { + "name" => "subjects_taught", + "outcome" => false + } + ], + "verifier" => { + "dfe_sign_in_uid" => "123", + "first_name" => "Seymoure", + "last_name" => "Skinner", + "email" => "seymore.skinner@springfield-elementary.edu" + }, + "created_at" => "2024-01-01T12:00:00.000+00:00" + } + ) + end + end +end diff --git a/spec/lib/dfe_sign_in/api/user_spec.rb b/spec/lib/dfe_sign_in/api/user_spec.rb index 4930089955..3a427bd183 100644 --- a/spec/lib/dfe_sign_in/api/user_spec.rb +++ b/spec/lib/dfe_sign_in/api/user_spec.rb @@ -63,7 +63,7 @@ end end - context "with an invalid response" do + context "with a invalid response" do before do stub_failed_dfe_sign_in_user_info_request(999, 456) end @@ -76,4 +76,44 @@ ) end end + + describe "#service_access?" do + subject { user.service_access? } + + context "when the response is successful" do + before do + stub_dfe_sign_in_user_info_request(999, 456, "my_role") + end + + it { is_expected.to be true } + end + + context "when the response is not successful" do + before do + stub_failed_dfe_sign_in_user_info_request(999, 456, status: 404) + end + + it { is_expected.to be false } + end + end + + describe "#service_error?" do + subject { user.service_error? } + + context "when the response is not a server error" do + before do + stub_dfe_sign_in_user_info_request(999, 456, "my_role") + end + + it { is_expected.to be false } + end + + context "when the response is an error" do + before do + stub_failed_dfe_sign_in_user_info_request(999, 456, status: 500) + end + + it { is_expected.to be true } + end + end end diff --git a/spec/models/journeys/further_education_payments/provider/authorisation_spec.rb b/spec/models/journeys/further_education_payments/provider/authorisation_spec.rb new file mode 100644 index 0000000000..39ddc72784 --- /dev/null +++ b/spec/models/journeys/further_education_payments/provider/authorisation_spec.rb @@ -0,0 +1,91 @@ +require "rails_helper" + +RSpec.describe Journeys::FurtherEducationPayments::Provider::Authorisation do + let(:eligibility) { create(:further_education_payments_eligibility) } + + let(:organisation) { eligibility.school } + + let(:claim) { eligibility.claim } + + let(:journey_session) do + create( + :further_education_payments_provider_session, + answers: answers.merge(claim_id: claim.id) + ) + end + + let(:authorisation) do + described_class.new(answers: journey_session.answers, slug: "verify-claim") + end + + describe "#failure_reason" do + subject { authorisation.failure_reason } + + context "when the ukprns don't match" do + let(:answers) do + { + dfe_sign_in_service_access: true, + dfe_sign_in_organisation_ukprn: "mismatch" + } + end + + it { is_expected.to eq(:organisation_mismatch) } + end + + context "when the user does not have access to the service" do + let(:answers) do + { + dfe_sign_in_service_access: false, + dfe_sign_in_organisation_ukprn: organisation.ukprn + } + end + + it { is_expected.to eq(:no_service_access) } + end + + context "when the user does not have the required role" do + let(:answers) do + { + dfe_sign_in_service_access: true, + dfe_sign_in_organisation_ukprn: organisation.ukprn, + dfe_sign_in_role_codes: ["incorrect_role"] + } + end + + it { is_expected.to eq(:incorrect_role) } + end + + context "when the user is a claim admin" do + let(:answers) do + { + dfe_sign_in_service_access: true, + dfe_sign_in_organisation_ukprn: organisation.ukprn, + dfe_sign_in_role_codes: [ + DfeSignIn::User::SERVICE_OPERATOR_DFE_SIGN_IN_ROLE_CODE, + Journeys::FurtherEducationPayments::Provider::CLAIM_VERIFIER_DFE_SIGN_IN_ROLE_CODE + ] + } + end + + it { is_expected.to eq(:claim_admin) } + end + + context "when the claim has already been verified" do + let(:answers) do + { + dfe_sign_in_service_access: true, + dfe_sign_in_organisation_ukprn: organisation.ukprn, + dfe_sign_in_role_codes: [ + Journeys::FurtherEducationPayments::Provider::CLAIM_VERIFIER_DFE_SIGN_IN_ROLE_CODE + ] + } + end + + let(:eligibility) do + create(:further_education_payments_eligibility, :verified) + end + + it { is_expected.to eq(:already_verified) } + end + end +end diff --git a/spec/models/journeys_spec.rb b/spec/models/journeys_spec.rb index 7530daada1..a7ac57d110 100644 --- a/spec/models/journeys_spec.rb +++ b/spec/models/journeys_spec.rb @@ -10,6 +10,7 @@ Journeys::TeacherStudentLoanReimbursement, Journeys::GetATeacherRelocationPayment, Journeys::FurtherEducationPayments, + Journeys::FurtherEducationPayments::Provider, Journeys::EarlyYearsPayment::Provider::Start, Journeys::EarlyYearsPayment::Provider::Authenticated ]) @@ -23,6 +24,7 @@ Journeys::TeacherStudentLoanReimbursement::ROUTING_NAME, Journeys::GetATeacherRelocationPayment::ROUTING_NAME, Journeys::FurtherEducationPayments::ROUTING_NAME, + Journeys::FurtherEducationPayments::Provider::ROUTING_NAME, Journeys::EarlyYearsPayment::Provider::Start::ROUTING_NAME, Journeys::EarlyYearsPayment::Provider::Authenticated::ROUTING_NAME ]) diff --git a/spec/support/dfe_sign_in_helpers.rb b/spec/support/dfe_sign_in_helpers.rb index 205d5a25ca..7d61c15b44 100644 --- a/spec/support/dfe_sign_in_helpers.rb +++ b/spec/support/dfe_sign_in_helpers.rb @@ -73,23 +73,23 @@ def dfe_sign_in_auth_hash(attributes) }.deep_merge(attributes.deep_stringify_keys) end - def stub_dfe_sign_in_user_info_request(user_id, organisation_id, role_code) + def stub_dfe_sign_in_user_info_request(user_id, organisation_id, role_code, service_id: "XXXXXXX") url = dfe_sign_in_user_info_url(user_id, organisation_id) api_response = { "userId" => user_id, "serviceId" => "XXXXXXX", "organisationId" => organisation_id, - "roles" => [ + "roles" => Array.wrap(role_code).map.each_with_index do |code, i| { "id" => "YYYYYYY", "name" => "Access to Teacher Payments", - "code" => role_code, + "code" => code, "numericId" => "162", "status" => { - "id" => 1 + "id" => i + 1 } } - ], + end, "identifiers" => [ { "key" => "groups", @@ -102,14 +102,14 @@ def stub_dfe_sign_in_user_info_request(user_id, organisation_id, role_code) .to_return(status: 200, body: api_response) end - def stub_failed_dfe_sign_in_user_info_request(user_id, organisation_id) + def stub_failed_dfe_sign_in_user_info_request(user_id, organisation_id, status: 500) url = dfe_sign_in_user_info_url(user_id, organisation_id) api_response = { error: "An error occurred" }.to_json stub_request(:get, url) - .to_return(status: 500, body: api_response) + .to_return(status: status, body: api_response) end def stub_dfe_sign_in_user_list_request(number_of_pages: 1, page_number: nil)