diff --git a/app/controllers/claims/schools/claims/add_claim_controller.rb b/app/controllers/claims/schools/claims/add_claim_controller.rb
new file mode 100644
index 000000000..993963478
--- /dev/null
+++ b/app/controllers/claims/schools/claims/add_claim_controller.rb
@@ -0,0 +1,46 @@
+class Claims::Schools::Claims::AddClaimController < Claims::ApplicationController
+ include WizardController
+ include Claims::BelongsToSchool
+
+ before_action :has_school_accepted_grant_conditions?
+ before_action :set_wizard
+ before_action :authorize_claim
+
+ helper_method :index_path
+
+ def update
+ if !@wizard.save_step
+ render "edit"
+ elsif @wizard.next_step.present?
+ redirect_to step_path(@wizard.next_step)
+ elsif @wizard.valid?
+ @wizard.create_claim
+ @wizard.reset_state
+ redirect_to confirmation_claims_school_claim_path(@school, @wizard.claim)
+ else
+ redirect_to rejected_claims_school_claims_path(@school)
+ end
+ end
+
+ private
+
+ def set_wizard
+ state = session[state_key] ||= {}
+ current_step = params[:step]&.to_sym
+ @wizard = Claims::AddClaimWizard.new(
+ school: @school, created_by: current_user, params:, state:, current_step:,
+ )
+ end
+
+ def authorize_claim
+ authorize Claims::Claim, :create?
+ end
+
+ def step_path(step)
+ add_claim_claims_school_claims_path(state_key:, step:)
+ end
+
+ def index_path
+ claims_school_claims_path(@school)
+ end
+end
diff --git a/app/controllers/claims/schools/claims_controller.rb b/app/controllers/claims/schools/claims_controller.rb
index 9424b9449..c4f2877a6 100644
--- a/app/controllers/claims/schools/claims_controller.rb
+++ b/app/controllers/claims/schools/claims_controller.rb
@@ -2,7 +2,7 @@ class Claims::Schools::ClaimsController < Claims::ApplicationController
include Claims::BelongsToSchool
before_action :has_school_accepted_grant_conditions?
- before_action :set_claim, only: %i[show check confirmation submit edit update rejected create_revision remove destroy]
+ before_action :set_claim, only: %i[show check confirmation submit edit update create_revision remove destroy]
before_action :authorize_claim
before_action :get_valid_revision, only: :check
@@ -12,16 +12,6 @@ def index
@pagy, @claims = pagy(@school.claims.active.order_created_at_desc)
end
- def new; end
-
- def create
- if claim_provider_form.save
- redirect_to new_claims_school_claim_mentors_path(@school, claim_provider_form.claim)
- else
- render :new
- end
- end
-
def create_revision
revision = Claims::Claim::CreateRevision.call(claim: @claim)
redirect_to edit_claims_school_claim_path(@school, revision)
@@ -103,7 +93,7 @@ def claim_id
end
def authorize_claim
- authorize @claim || Claims::Claim
+ authorize @claim || Claims::Claim.new
end
def get_valid_revision
diff --git a/app/controllers/claims/support/schools/claims/add_claim_controller.rb b/app/controllers/claims/support/schools/claims/add_claim_controller.rb
new file mode 100644
index 000000000..b3cb3f992
--- /dev/null
+++ b/app/controllers/claims/support/schools/claims/add_claim_controller.rb
@@ -0,0 +1,48 @@
+class Claims::Support::Schools::Claims::AddClaimController < Claims::Support::ApplicationController
+ include WizardController
+ include Claims::BelongsToSchool
+
+ before_action :has_school_accepted_grant_conditions?
+ before_action :set_wizard
+ before_action :authorize_claim
+
+ helper_method :index_path
+
+ def update
+ if !@wizard.save_step
+ render "edit"
+ elsif @wizard.next_step.present?
+ redirect_to step_path(@wizard.next_step)
+ elsif @wizard.valid?
+ @wizard.create_claim
+ @wizard.reset_state
+ redirect_to claims_support_school_claims_path(@school), flash: {
+ heading: t(".success"),
+ }
+ else
+ redirect_to rejected_claims_support_school_claims_path(@school)
+ end
+ end
+
+ private
+
+ def set_wizard
+ state = session[state_key] ||= {}
+ current_step = params[:step]&.to_sym
+ @wizard = Claims::AddClaimWizard.new(
+ school: @school, created_by: current_user, params:, state:, current_step:,
+ )
+ end
+
+ def authorize_claim
+ authorize Claims::Claim, :create?
+ end
+
+ def step_path(step)
+ add_claim_claims_support_school_claims_path(state_key:, step:)
+ end
+
+ def index_path
+ claims_support_school_claims_path(@school)
+ end
+end
diff --git a/app/controllers/claims/support/schools/claims_controller.rb b/app/controllers/claims/support/schools/claims_controller.rb
index 8f35c41b5..6f40b984d 100644
--- a/app/controllers/claims/support/schools/claims_controller.rb
+++ b/app/controllers/claims/support/schools/claims_controller.rb
@@ -1,7 +1,7 @@
class Claims::Support::Schools::ClaimsController < Claims::Support::ApplicationController
include Claims::BelongsToSchool
- before_action :set_claim, only: %i[check draft show edit update remove destroy rejected create_revision]
+ before_action :set_claim, only: %i[check draft show edit update remove destroy create_revision]
before_action :authorize_claim
before_action :get_valid_revision, only: :check
@@ -11,8 +11,6 @@ def index
@pagy, @claims = pagy(@school.claims.active.order_created_at_desc)
end
- def new; end
-
def show; end
def remove; end
@@ -25,14 +23,6 @@ def destroy
}
end
- def create
- if claim_provider_form.save
- redirect_to new_claims_support_school_claim_mentors_path(@school, claim_provider_form.claim)
- else
- render :new
- end
- end
-
def create_revision
revision = Claims::Claim::CreateRevision.call(claim: @claim)
redirect_to edit_claims_support_school_claim_path(@school, revision)
@@ -112,7 +102,7 @@ def claim_provider_form
end
def authorize_claim
- authorize @claim || Claims::Claim
+ authorize @claim || Claims::Claim.new
end
def get_valid_revision
diff --git a/app/models/claims/claim.rb b/app/models/claims/claim.rb
index 963e8a71e..a5ed7cc85 100644
--- a/app/models/claims/claim.rb
+++ b/app/models/claims/claim.rb
@@ -49,6 +49,8 @@ class Claims::Claim < ApplicationRecord
has_many :mentor_trainings, dependent: :destroy
has_many :mentors, through: :mentor_trainings
+ accepts_nested_attributes_for :mentor_trainings
+
validates :status, presence: true
validates(
:reference,
diff --git a/app/policies/claims/claim_policy.rb b/app/policies/claims/claim_policy.rb
index 492af658e..314214ba9 100644
--- a/app/policies/claims/claim_policy.rb
+++ b/app/policies/claims/claim_policy.rb
@@ -33,7 +33,7 @@ def confirmation?
# TODO: Remove record.draft? and not create drafts for existing drafts
def draft?
- current_claim_window? && user.support_user? && (record.internal_draft? || record.draft?)
+ current_claim_window? && user.support_user? && (record.internal_draft? || record.draft? || record.new_record?)
end
def check?
diff --git a/app/services/claims/claim/calculate_amount.rb b/app/services/claims/claim/calculate_amount.rb
index a80823751..03e52d820 100644
--- a/app/services/claims/claim/calculate_amount.rb
+++ b/app/services/claims/claim/calculate_amount.rb
@@ -9,7 +9,7 @@ def call
region = claim.school.region
claims_funding_available_per_hour_pence = region.claims_funding_available_per_hour_pence
- total_hours_completed = claim.mentor_trainings.sum(:hours_completed)
+ total_hours_completed = claim.mentor_trainings.filter_map(&:hours_completed).sum
amount_in_pence = claims_funding_available_per_hour_pence * total_hours_completed
diff --git a/app/services/claims/claim/create_draft.rb b/app/services/claims/claim/create_draft.rb
index f3b08c40f..8ad6876af 100644
--- a/app/services/claims/claim/create_draft.rb
+++ b/app/services/claims/claim/create_draft.rb
@@ -1,4 +1,6 @@
class Claims::Claim::CreateDraft < ApplicationService
+ include Claims::Claim::Referencable
+
def initialize(claim:)
@claim = claim
end
@@ -33,9 +35,4 @@ def updated_claim
claim
end
end
-
- def generate_reference
- reference = SecureRandom.random_number(99_999_999) while Claims::Claim.exists?(reference:)
- reference
- end
end
diff --git a/app/services/claims/claim/submit.rb b/app/services/claims/claim/submit.rb
index 56297f992..586e75e88 100644
--- a/app/services/claims/claim/submit.rb
+++ b/app/services/claims/claim/submit.rb
@@ -1,4 +1,6 @@
class Claims::Claim::Submit < ApplicationService
+ include Claims::Claim::Referencable
+
def initialize(claim:, user:)
@claim = claim
@user = user
@@ -39,9 +41,4 @@ def updated_claim
claim
end
end
-
- def generate_reference
- reference = SecureRandom.random_number(99_999_999) while Claims::Claim.exists?(reference:)
- reference
- end
end
diff --git a/app/services/concerns/claims/claim/referencable.rb b/app/services/concerns/claims/claim/referencable.rb
new file mode 100644
index 000000000..233111ccf
--- /dev/null
+++ b/app/services/concerns/claims/claim/referencable.rb
@@ -0,0 +1,13 @@
+module Claims::Claim::Referencable
+ extend ActiveSupport::Concern
+
+ included do
+ def generate_reference
+ loop do
+ reference = SecureRandom.random_number(99_999_999)
+
+ break reference unless Claims::Claim.exists?(reference:)
+ end
+ end
+ end
+end
diff --git a/app/views/claims/schools/claims/add_claim/edit.html.erb b/app/views/claims/schools/claims/add_claim/edit.html.erb
new file mode 100644
index 000000000..259fbe83c
--- /dev/null
+++ b/app/views/claims/schools/claims/add_claim/edit.html.erb
@@ -0,0 +1,13 @@
+<% render "claims/schools/primary_navigation", school: @school, current: :claims %>
+
+<%= content_for(:before_content) do %>
+ <%= govuk_back_link(href: back_link_path) %>
+<% end %>
+
+
+ <%= render_wizard(@wizard, contextual_text: t(".caption")) %>
+
+
+ <%= govuk_link_to(t(".cancel"), index_path, no_visited_state: true) %>
+
+
diff --git a/app/views/claims/schools/claims/index.html.erb b/app/views/claims/schools/claims/index.html.erb
index 5f801240a..ccef17f18 100644
--- a/app/views/claims/schools/claims/index.html.erb
+++ b/app/views/claims/schools/claims/index.html.erb
@@ -11,7 +11,7 @@
<%= sanitize t(".closing_date_disclaimer", time: l(Claims::ClaimWindow.current.ends_on.end_of_day, format: :time_on_date)) %>
<% end %>
- <%= govuk_link_to t(".add_claim"), new_claims_school_claim_path, class: "govuk-button" %>
+ <%= govuk_button_link_to(t(".add_claim"), new_add_claim_claims_school_claims_path) %>
<% else %>
<%= govuk_inset_text text: t(".add_mentor_guidance_html", link_to: govuk_link_to(t(".add_a_mentor"), claims_school_mentors_path(@school))) %>
<% end %>
diff --git a/app/views/claims/support/schools/claims/add_claim/edit.html.erb b/app/views/claims/support/schools/claims/add_claim/edit.html.erb
new file mode 100644
index 000000000..728ff1db2
--- /dev/null
+++ b/app/views/claims/support/schools/claims/add_claim/edit.html.erb
@@ -0,0 +1,13 @@
+<% render "claims/support/primary_navigation", current: :organisations %>
+
+<%= content_for(:before_content) do %>
+ <%= govuk_back_link(href: back_link_path) %>
+<% end %>
+
+
+ <%= render_wizard(@wizard, contextual_text: t(".caption", school_name: @school.name)) %>
+
+
+ <%= govuk_link_to(t(".cancel"), index_path, no_visited_state: true) %>
+
+
diff --git a/app/views/claims/support/schools/claims/index.html.erb b/app/views/claims/support/schools/claims/index.html.erb
index e4cb316ef..21be7981b 100644
--- a/app/views/claims/support/schools/claims/index.html.erb
+++ b/app/views/claims/support/schools/claims/index.html.erb
@@ -10,7 +10,7 @@
<% if @school.mentors.any? %>
<%= govuk_inset_text text: t(".guidance", start_year: Claims::ClaimWindow.current.academic_year.starts_on.year, end_year: Claims::ClaimWindow.current.academic_year.ends_on.year) %>
- <%= govuk_button_link_to t(".add_claim"), new_claims_support_school_claim_path %>
+ <%= govuk_button_link_to t(".add_claim"), new_add_claim_claims_support_school_claims_path %>
<% else %>
<%= govuk_inset_text text: t(".add_mentor_guidance_html", link_to: govuk_link_to(t(".add_a_mentor"), claims_support_school_mentors_path(@school))) %>
<% end %>
diff --git a/app/views/claims/support/schools/claims/mentors/_form.html.erb b/app/views/claims/support/schools/claims/mentors/_form.html.erb
index 8f2884689..b09e0abbc 100644
--- a/app/views/claims/support/schools/claims/mentors/_form.html.erb
+++ b/app/views/claims/support/schools/claims/mentors/_form.html.erb
@@ -33,7 +33,7 @@
- <%= govuk_link_to t(".change_provider"), new_claims_school_claim_path(school, id: claim_mentors_form.claim.id) %>
+ <%= govuk_link_to t(".change_provider"), new_claims_support_school_claim_path(school, id: claim_mentors_form.claim.id, claim_id: claim_mentors_form.claim.id) %>
<% end %>
diff --git a/app/views/wizards/claims/add_claim_wizard/_check_your_answers_step.html.erb b/app/views/wizards/claims/add_claim_wizard/_check_your_answers_step.html.erb
new file mode 100644
index 000000000..77b4ce82b
--- /dev/null
+++ b/app/views/wizards/claims/add_claim_wizard/_check_your_answers_step.html.erb
@@ -0,0 +1,114 @@
+<% content_for :page_title, title_with_error_prefix(
+ t(".page_title", contextual_text:),
+ error: current_step.errors.any?,
+) %>
+
+<%= form_for(current_step, url: current_step_path, method: :put) do |f| %>
+ <%= f.govuk_error_summary %>
+
+
+
+
<%= contextual_text %>
+
<%= t(".title") %>
+
+ <%= govuk_summary_list do |summary_list| %>
+ <% unless current_user.support_user? %>
+ <% summary_list.with_row do |row| %>
+ <% row.with_key(text: Claims::Claim.human_attribute_name(:school)) %>
+ <% row.with_value(text: @wizard.school.name) %>
+ <% end %>
+ <% end %>
+
+ <% summary_list.with_row do |row| %>
+ <% row.with_key(text: Claims::ClaimWindow.human_attribute_name(:academic_year)) %>
+ <% row.with_value(text: @wizard.academic_year.name) %>
+ <% end %>
+
+ <% summary_list.with_row do |row| %>
+ <% row.with_key(text: Claims::Claim.human_attribute_name(:accredited_provider)) %>
+ <% row.with_value(text: @wizard.provider.name) %>
+ <% row.with_action(text: t("change"),
+ href: step_path(:provider),
+ visually_hidden_text: Claims::Claim.human_attribute_name(:accredited_provider),
+ html_attributes: {
+ class: "govuk-link--no-visited-state",
+ }) %>
+ <% end %>
+
+ <% summary_list.with_row do |row| %>
+ <% row.with_key(text: Claims::Claim.human_attribute_name(:mentors)) %>
+ <% row.with_value do %>
+
+ <% @wizard.steps[:mentor].selected_mentors.each do |mentor| %>
+ - <%= mentor.full_name %>
+ <% end %>
+
+ <% end %>
+ <% row.with_action(text: t("change"),
+ href: step_path(:mentor),
+ visually_hidden_text: Claims::Claim.human_attribute_name(:mentors),
+ html_attributes: {
+ class: "govuk-link--no-visited-state",
+ }) %>
+ <% end %>
+ <% end %>
+
+
<%= t(".hours_of_training") %>
+
+ <%= govuk_summary_list do |summary_list| %>
+ <% @wizard.steps[:mentor].selected_mentors.each do |mentor| %>
+ <% mentor_training = @wizard.steps[@wizard.step_name_for_mentor(mentor)] %>
+ <% summary_list.with_row do |row| %>
+ <% row.with_key(text: mentor_training.mentor.full_name) %>
+ <% row.with_value(
+ text: pluralize(
+ mentor_training.hours_completed,
+ t(".hour"),
+ ),
+ ) %>
+ <% row.with_action(
+ text: t("change"),
+ href: step_path(@wizard.step_name_for_mentor(mentor)),
+ visually_hidden_text: "Hours of training for #{mentor.full_name}",
+ html_attributes: { class: "govuk-link--no-visited-state" },
+ ) %>
+ <% end %>
+ <% end %>
+ <% end %>
+
+
<%= t(".grant_funding") %>
+
+ <%= govuk_summary_list(actions: false) do |summary_list| %>
+ <% summary_list.with_row do |row| %>
+ <% row.with_key(text: t(".total_hours")) %>
+ <% row.with_value(text: pluralize(@wizard.total_hours, t(".hour"))) %>
+ <% end %>
+
+ <% summary_list.with_row do |row| %>
+ <% row.with_key(text: t(".hourly_rate")) %>
+ <% row.with_value(text: humanized_money_with_symbol(@wizard.school.region.funding_available_per_hour)) %>
+ <% end %>
+
+ <% summary_list.with_row do |row| %>
+ <% row.with_key(text: Claims::Claim.human_attribute_name(:claim_amount)) %>
+ <% row.with_value(text: humanized_money_with_symbol(@wizard.claim.amount)) %>
+ <% end %>
+ <% end %>
+
+ <% if !current_user.support_user? %>
+
<%= t(".disclaimer") %>
+
+
+ - <%= t(".claim_on_behalf_of_school") %>
+ - <%= t(".read_and_accepted_grant_conditions") %>
+ - <%= t(".accurate_information") %>
+ - <%= t(".provide_evidence") %>
+
+
+ <%= govuk_warning_text(text: t(".warning")) %>
+ <% end %>
+
+ <%= f.govuk_submit current_user.support_user? ? t(".save") : t(".submit") %>
+
+
+<% end %>
diff --git a/app/views/wizards/claims/add_claim_wizard/_mentor_step.html.erb b/app/views/wizards/claims/add_claim_wizard/_mentor_step.html.erb
new file mode 100644
index 000000000..7a2f84281
--- /dev/null
+++ b/app/views/wizards/claims/add_claim_wizard/_mentor_step.html.erb
@@ -0,0 +1,30 @@
+<% content_for :page_title, title_with_error_prefix(
+ t(".page_title", contextual_text:, provider_name: @wizard.provider.name),
+ error: current_step.errors.any?,
+) %>
+
+<%= form_for(current_step, url: current_step_path, method: :put) do |f| %>
+ <%= f.govuk_error_summary %>
+
+
+
+ <%= contextual_text %>
+
<%= t(".heading", provider_name: @wizard.provider.name) %>
+
+ <%= render Claims::Claim::MentorsForm::DisclaimerComponent.new(mentors_form: current_step) %>
+
+ <%= f.govuk_collection_check_boxes(
+ :mentor_ids,
+ current_step.mentors_with_claimable_hours,
+ :id, :full_name, :trn,
+ legend: {
+ size: "s",
+ text: t(".label"),
+ },
+ hint: { text: t(".select_all_that_apply") }
+ ) %>
+
+ <%= f.govuk_submit t(".continue") %>
+
+
+<% end %>
diff --git a/app/views/wizards/claims/add_claim_wizard/_mentor_training_step.html.erb b/app/views/wizards/claims/add_claim_wizard/_mentor_training_step.html.erb
new file mode 100644
index 000000000..ca815bab0
--- /dev/null
+++ b/app/views/wizards/claims/add_claim_wizard/_mentor_training_step.html.erb
@@ -0,0 +1,42 @@
+<% content_for :page_title, title_with_error_prefix(
+ t(".page_title", contextual_text:, provider_name: @wizard.provider.name, mentor: current_step.mentor.full_name),
+ error: current_step.errors.any?,
+) %>
+
+<%= form_for(current_step, url: current_step_path, method: :put) do |f| %>
+ <%= f.govuk_error_summary %>
+
+
+
+ <%= contextual_text %> - <%= @wizard.steps[:provider].provider.name %>
+
<%= t(".hours_of_training_for_mentor", mentor: current_step.mentor.full_name) %>
+
+ <%= render Claims::Claim::MentorTrainingForm::DisclaimerComponent.new(mentor_training_form: current_step) %>
+
+ <%= f.govuk_radio_buttons_fieldset(
+ :hours_to_claim,
+ legend: {
+ size: "m",
+ text: t(".hours_of_training"),
+ },
+ ) do %>
+ <%= f.govuk_radio_button :hours_to_claim, "maximum", label: { text: t(".hours", count: current_step.max_hours) }, hint: { text: t(".hours_hint.#{current_step.max_hours == 20 ? "full" : "remaining"}", count: current_step.max_hours) } %>
+
+ <%= f.govuk_radio_divider %>
+
+ <%= f.govuk_radio_button(:hours_to_claim, "custom", label: { text: t(".other_amount") }) do %>
+ <%= f.govuk_number_field(
+ :custom_hours,
+ class: "govuk-input--width-2",
+ min: 1,
+ max: current_step.max_hours,
+ label: { text: t(".number_of_hours"), class: "govuk-!-font-weight-bold" },
+ hint: { text: t(".custom_hours_completed_hint", count: current_step.max_hours) },
+ ) %>
+ <% end %>
+ <% end %>
+
+ <%= f.govuk_submit t("continue") %>
+
+
+<% end %>
diff --git a/app/views/wizards/claims/add_claim_wizard/_no_mentors_step.html.erb b/app/views/wizards/claims/add_claim_wizard/_no_mentors_step.html.erb
new file mode 100644
index 000000000..d525b1046
--- /dev/null
+++ b/app/views/wizards/claims/add_claim_wizard/_no_mentors_step.html.erb
@@ -0,0 +1,18 @@
+<% content_for :page_title, title_with_error_prefix(
+ t(".page_title", contextual_text:, provider_name: @wizard.provider.name),
+ error: current_step.errors.any?,
+) %>
+
+
+
+
<%= t(".heading_empty", provider_name: @wizard.provider.name) %>
+
+
+ <%= t(".no_mentors_with_claimable_hours", provider_name: @wizard.provider.name) %>
+
+
+
+ <%= govuk_link_to t(".change_provider"), step_path(:provider) %>
+
+
+
diff --git a/app/views/wizards/claims/add_claim_wizard/_provider_step.html.erb b/app/views/wizards/claims/add_claim_wizard/_provider_step.html.erb
new file mode 100644
index 000000000..22c61bdfd
--- /dev/null
+++ b/app/views/wizards/claims/add_claim_wizard/_provider_step.html.erb
@@ -0,0 +1,23 @@
+<% content_for :page_title, title_with_error_prefix(
+ t(".page_title", contextual_text:),
+ error: current_step.errors.any?,
+) %>
+
+<%= form_for(current_step, url: current_step_path, method: :put) do |f| %>
+ <%= f.govuk_error_summary %>
+
+
+
+ <%= contextual_text %>
+
+ <%= f.govuk_collection_radio_buttons(
+ :id,
+ current_step.providers_for_selection,
+ :id, :name,
+ legend: { size: "l", text: t(".title"), tag: "h1" }
+ ) %>
+
+ <%= f.govuk_submit t(".continue") %>
+
+
+<% end %>
diff --git a/app/wizards/claims/add_claim_wizard.rb b/app/wizards/claims/add_claim_wizard.rb
new file mode 100644
index 000000000..1cfb5cb1e
--- /dev/null
+++ b/app/wizards/claims/add_claim_wizard.rb
@@ -0,0 +1,106 @@
+module Claims
+ class AddClaimWizard < BaseWizard
+ attr_reader :school, :created_by
+
+ def initialize(school:, created_by:, params:, state:, current_step: nil)
+ @school = school
+ @created_by = created_by
+ super(state:, params:, current_step:)
+ end
+
+ def define_steps
+ add_step(ProviderStep)
+ if mentors_with_claimable_hours.any? || current_step == :check_your_answers
+ add_step(MentorStep)
+ # Loop over mentors
+ steps.fetch(:mentor).selected_mentors.each do |mentor|
+ add_step(MentorTrainingStep, { mentor_id: mentor.id })
+ end
+ add_step(CheckYourAnswersStep)
+ else
+ add_step(NoMentorsStep)
+ end
+ end
+
+ def add_step(step_class, preset_attributes = {})
+ name = step_name(step_class, preset_attributes[:mentor_id])
+ attributes = step_attributes(name, step_class, preset_attributes)
+ @steps[name] = step_class.new(wizard: self, attributes:)
+ end
+
+ def academic_year
+ Claims::ClaimWindow.current.academic_year
+ end
+
+ def total_hours
+ mentor_training_steps.map(&:hours_completed).sum
+ end
+
+ def claim
+ @claim ||= Claims::Claim.new(
+ provider:,
+ school:,
+ created_by:,
+ claim_window: Claims::ClaimWindow.current,
+ mentor_trainings_attributes: mentor_training_steps.map do |mentor_training_step|
+ {
+ mentor_id: mentor_training_step.mentor_id,
+ hours_completed: mentor_training_step.hours_completed,
+ provider:,
+ }
+ end,
+ )
+ end
+
+ def create_claim
+ if created_by.support_user?
+ Claims::Claim::CreateDraft.call(claim:)
+ else
+ Claims::Claim::Submit.call(claim:, user: created_by)
+ end
+ end
+
+ def mentors_with_claimable_hours
+ return Claims::Mentor.none if provider.blank?
+
+ @mentors_with_claimable_hours ||= Claims::MentorsWithRemainingClaimableHoursQuery.call(
+ params: {
+ school:,
+ provider:,
+ claim: Claims::Claim.new(academic_year:),
+ },
+ )
+ end
+
+ def provider
+ steps.fetch(:provider).provider
+ end
+
+ def step_name_for_mentor(mentor)
+ step_name(MentorTrainingStep, mentor.id)
+ end
+
+ private
+
+ def step_name(step_class, id = nil)
+ # e.g. YearGroupStep becomes :year_group
+ name = super(step_class)
+ return name.to_sym if id.blank?
+
+ # e.g. with id it becomes :year_group_#{id}
+ "#{name}_#{id}".to_sym
+ end
+
+ def step_attributes(name, step_class, preset_attributes = {})
+ attributes = super(name, step_class)
+ return attributes if preset_attributes.blank?
+
+ attributes = {} if attributes.blank?
+ attributes.merge(preset_attributes)
+ end
+
+ def mentor_training_steps
+ steps.values.select { |step| step.is_a?(MentorTrainingStep) }
+ end
+ end
+end
diff --git a/app/wizards/claims/add_claim_wizard/check_your_answers_step.rb b/app/wizards/claims/add_claim_wizard/check_your_answers_step.rb
new file mode 100644
index 000000000..70f9df247
--- /dev/null
+++ b/app/wizards/claims/add_claim_wizard/check_your_answers_step.rb
@@ -0,0 +1,2 @@
+class Claims::AddClaimWizard::CheckYourAnswersStep < BaseStep
+end
diff --git a/app/wizards/claims/add_claim_wizard/mentor_step.rb b/app/wizards/claims/add_claim_wizard/mentor_step.rb
new file mode 100644
index 000000000..81e2726d2
--- /dev/null
+++ b/app/wizards/claims/add_claim_wizard/mentor_step.rb
@@ -0,0 +1,21 @@
+class Claims::AddClaimWizard::MentorStep < BaseStep
+ attribute :mentor_ids, default: []
+
+ validates :mentor_ids, presence: true, inclusion: { in: ->(step) { step.mentors_with_claimable_hours.unscoped.ids } }
+
+ delegate :school, :claim, :mentors_with_claimable_hours, to: :wizard
+
+ def selected_mentors
+ return Claims::Mentor.none if mentors_with_claimable_hours.nil?
+
+ @selected_mentors ||= Claims::Mentor.where(id: mentor_ids).order_by_full_name
+ end
+
+ def all_school_mentors_visible?
+ @all_school_mentors_visible ||= school.mentors.count == mentors_with_claimable_hours.count
+ end
+
+ def mentor_ids=(value)
+ super Array(value).compact_blank
+ end
+end
diff --git a/app/wizards/claims/add_claim_wizard/mentor_training_step.rb b/app/wizards/claims/add_claim_wizard/mentor_training_step.rb
new file mode 100644
index 000000000..e1ac0bb8f
--- /dev/null
+++ b/app/wizards/claims/add_claim_wizard/mentor_training_step.rb
@@ -0,0 +1,56 @@
+class Claims::AddClaimWizard::MentorTrainingStep < BaseStep
+ attribute :mentor_id
+ attribute :hours_to_claim, :string
+ attribute :custom_hours
+
+ HOURS_TO_CLAIM = %w[maximum custom].freeze
+
+ validates :mentor_id, presence: true
+ validates :hours_to_claim, presence: true, inclusion: { in: HOURS_TO_CLAIM }
+
+ validates(
+ :custom_hours,
+ presence: true,
+ numericality: { only_integer: true },
+ between: { min: 1, max: :max_hours },
+ if: :custom_hours_selected?,
+ )
+
+ delegate :full_name, to: :mentor, prefix: true
+ delegate :name, to: :provider, prefix: true
+ delegate :provider, to: :wizard
+
+ def initialize(wizard:, attributes:)
+ super
+
+ return if custom_hours_selected?
+
+ self.custom_hours = nil
+ end
+
+ def mentor
+ @mentor ||= @wizard.steps.fetch(:mentor).selected_mentors.find_by(id: mentor_id)
+ end
+
+ def max_hours
+ training_allowance.remaining_hours
+ end
+
+ def training_allowance
+ @training_allowance ||= Claims::TrainingAllowance.new(
+ mentor:,
+ provider:,
+ academic_year: @wizard.academic_year,
+ )
+ end
+
+ def hours_completed
+ (hours_to_claim == "maximum" ? max_hours : custom_hours).to_i
+ end
+
+ private
+
+ def custom_hours_selected?
+ hours_to_claim == "custom"
+ end
+end
diff --git a/app/wizards/claims/add_claim_wizard/no_mentors_step.rb b/app/wizards/claims/add_claim_wizard/no_mentors_step.rb
new file mode 100644
index 000000000..1b10c11f8
--- /dev/null
+++ b/app/wizards/claims/add_claim_wizard/no_mentors_step.rb
@@ -0,0 +1,2 @@
+class Claims::AddClaimWizard::NoMentorsStep < BaseStep
+end
diff --git a/app/wizards/claims/add_claim_wizard/provider_step.rb b/app/wizards/claims/add_claim_wizard/provider_step.rb
new file mode 100644
index 000000000..865ac1d74
--- /dev/null
+++ b/app/wizards/claims/add_claim_wizard/provider_step.rb
@@ -0,0 +1,13 @@
+class Claims::AddClaimWizard::ProviderStep < BaseStep
+ attribute :id
+
+ validates :id, presence: true, inclusion: { in: ->(step) { step.providers_for_selection.ids } }
+
+ def providers_for_selection
+ Claims::Provider.private_beta_providers.order_by_name.select(:id, :name)
+ end
+
+ def provider
+ @provider ||= Claims::Provider.private_beta_providers.find_by(id:)
+ end
+end
diff --git a/config/locales/en/activemodel.yml b/config/locales/en/activemodel.yml
index f7de13d64..99a2a70e6 100644
--- a/config/locales/en/activemodel.yml
+++ b/config/locales/en/activemodel.yml
@@ -135,6 +135,28 @@ en:
id:
already_added: "%{school_name} has already been added. Try another school"
blank: Select a school
+ claims/add_claim_wizard/provider_step:
+ attributes:
+ id:
+ blank: Select a provider
+ claims/add_claim_wizard/mentor_step:
+ attributes:
+ mentor_ids:
+ blank: Select a mentor
+ claims/add_claim_wizard/mentor_training_step:
+ attributes:
+ mentor_id:
+ blank: Select a mentor
+ custom_hours:
+ blank: Enter the number of hours
+ not_an_integer: Enter whole numbers only
+ between: Enter the number of hours between %{min} and %{max}
+ hours_to_claim:
+ blank: Select the number of hours
+ claims/add_claim_wizard/check_your_answers_step:
+ attributes:
+ base:
+ unclaimable: You cannot submit the claim
claims/claim/mentor_training_form:
attributes:
custom_hours_completed:
diff --git a/config/locales/en/claims/schools/claims/add_claim.yml b/config/locales/en/claims/schools/claims/add_claim.yml
new file mode 100644
index 000000000..2bf8a1a3e
--- /dev/null
+++ b/config/locales/en/claims/schools/claims/add_claim.yml
@@ -0,0 +1,8 @@
+en:
+ claims:
+ schools:
+ claims:
+ add_claim:
+ edit:
+ caption: Add claim
+ cancel: Cancel
diff --git a/config/locales/en/claims/support/schools/claims/add_claim.yml b/config/locales/en/claims/support/schools/claims/add_claim.yml
new file mode 100644
index 000000000..4d2d4ee48
--- /dev/null
+++ b/config/locales/en/claims/support/schools/claims/add_claim.yml
@@ -0,0 +1,11 @@
+en:
+ claims:
+ support:
+ schools:
+ claims:
+ add_claim:
+ edit:
+ caption: Add claim - %{school_name}
+ cancel: Cancel
+ update:
+ success: Claim added
diff --git a/config/locales/en/wizards/claims/add_claim_wizard.yml b/config/locales/en/wizards/claims/add_claim_wizard.yml
new file mode 100644
index 000000000..633381858
--- /dev/null
+++ b/config/locales/en/wizards/claims/add_claim_wizard.yml
@@ -0,0 +1,51 @@
+en:
+ wizards:
+ claims:
+ add_claim_wizard:
+ provider_step:
+ continue: Continue
+ page_title: Accredited provider - %{contextual_text}
+ title: Accredited provider
+ mentor_step:
+ page_title: Mentors for %{provider_name} - %{contextual_text}
+ continue: Continue
+ label: Mentor
+ heading: Mentors for %{provider_name}
+ select_all_that_apply: Select all that apply
+ mentor_training_step:
+ page_title: Hours of training for %{mentor} - %{contextual_text} - %{provider_name}
+ hours_of_training_for_mentor: Hours of training for %{mentor}
+ hours_of_training: Hours of training
+ hours:
+ one: "%{count} hour"
+ other: "%{count} hours"
+ hours_hint:
+ full: The full amount of hours for standard training
+ remaining: The remaining amount of hours for standard training
+ other_amount: Another amount
+ custom_hours_completed_hint:
+ one: Enter whole numbers up to a maximum of %{count} hour
+ other: Enter whole numbers up to a maximum of %{count} hours
+ number_of_hours: Number of hours
+ check_your_answers_step:
+ page_title: Check your answers - %{contextual_text}
+ title: Check your answers
+ warning: You will not be able to change any of the claim details once you have submitted it.
+ submit: Submit claim
+ declaration: Declaration
+ disclaimer: "By submitting this claim, I confirm that:"
+ claim_on_behalf_of_school: I am authorised to claim on behalf of the school
+ read_and_accepted_grant_conditions: I have read and accepted the grant terms and conditions
+ accurate_information: the information detailed above is accurate and the total I am claiming back has been used to support the cost of the mentor training
+ provide_evidence: I will provide evidence to support this claim if requested by the Department for Education
+ total_hours: Total hours
+ hourly_rate: Hourly rate
+ hour: hour
+ grant_funding: Grant funding
+ hours_of_training: Hours of training
+ save: Save claim
+ no_mentors_step:
+ page_title: No mentors for %{provider_name} - %{contextual_text}
+ heading_empty: No mentors for %{provider_name}
+ no_mentors_with_claimable_hours: There are no mentors you can include in a claim because they have already had 20 hours of training claimed for with %{provider_name}.
+ change_provider: Change the accredited provider
diff --git a/config/routes/claims.rb b/config/routes/claims.rb
index 6933ba0d1..f0eaa401a 100644
--- a/config/routes/claims.rb
+++ b/config/routes/claims.rb
@@ -15,7 +15,14 @@
resources :schools, only: %i[index show] do
scope module: :schools do
- resources :claims do
+ resources :claims, except: %i[new create] do
+ collection do
+ get "new", to: "claims/add_claim#new", as: :new_add_claim
+ get "new/:state_key/:step", to: "claims/add_claim#edit", as: :add_claim
+ put "new/:state_key/:step", to: "claims/add_claim#update"
+ get :rejected
+ end
+
resource :mentors, only: %i[new create edit update], module: :claims do
member do
get :create_revision
@@ -31,7 +38,6 @@
get :remove
get :check
get :confirmation
- get :rejected
get :create_revision
post :submit
end
@@ -97,7 +103,14 @@
end
scope module: :schools do
- resources :claims do
+ resources :claims, except: %i[new create] do
+ collection do
+ get "new", to: "claims/add_claim#new", as: :new_add_claim
+ get "new/:state_key/:step", to: "claims/add_claim#edit", as: :add_claim
+ put "new/:state_key/:step", to: "claims/add_claim#update"
+ get :rejected
+ end
+
resource :mentors, only: %i[new create edit update], module: :claims do
member do
get :create_revision
@@ -112,7 +125,6 @@
member do
get :remove
get :check
- get :rejected
post :draft
get :create_revision
end
diff --git a/spec/policies/claims/claim_policy_spec.rb b/spec/policies/claims/claim_policy_spec.rb
index 117556f8e..19be80c00 100644
--- a/spec/policies/claims/claim_policy_spec.rb
+++ b/spec/policies/claims/claim_policy_spec.rb
@@ -7,7 +7,7 @@
let(:support_user) { build(:claims_support_user) }
let(:internal_draft_claim) { build(:claim) }
let(:draft_claim) { build(:claim, :draft) }
- let(:submitted_claim) { build(:claim, :submitted) }
+ let(:submitted_claim) { create(:claim, :submitted) }
before do
Claims::ClaimWindow::Build.call(claim_window_params: { starts_on: 2.days.ago, ends_on: 2.days.from_now }).save!(validate: false)
@@ -80,6 +80,12 @@
end
permissions :rejected? do
+ context "when user has an new claim (unsaved)" do
+ it "grants access" do
+ expect(claim_policy).to permit(user, Claims::Claim.new)
+ end
+ end
+
context "when user has an internal draft claim" do
it "grants access" do
expect(claim_policy).to permit(user, internal_draft_claim)
diff --git a/spec/system/claims/schools/claims/create_claim_spec.rb b/spec/system/claims/schools/claims/create_claim_spec.rb
index e1bd73f50..a9ba04472 100644
--- a/spec/system/claims/schools/claims/create_claim_spec.rb
+++ b/spec/system/claims/schools/claims/create_claim_spec.rb
@@ -143,12 +143,12 @@
when_i_click("Change Accredited provider")
when_i_choose_a_provider(bpn)
when_i_click("Continue")
- when_i_click("Change Mentors")
then_i_should_see_the_message("There are no mentors you can include in a claim because they have already had 20 hours of training claimed for with Best Practice Network.")
when_i_click("Change the accredited provider")
when_i_choose_a_provider(niot)
when_i_click("Continue")
when_i_click("Continue")
+ when_i_click("Continue")
then_i_should_land_on_the_check_page
end
@@ -309,9 +309,9 @@ def then_i_get_a_claim_reference_and_see_next_steps
expect(page).to have_content("We will process this claim at the end of September 2024 and all payments will be paid from December 2024.")
end
- def then_i_expect_the_training_hours_for(hours, mentor)
+ def then_i_expect_the_training_hours_for(_hours, mentor)
expect(page).to have_content("Hours of training for #{mentor.full_name}")
- find("#claims-claim-mentor-training-form-hours-completed-#{hours}-field").checked?
+ find("#claims-add-claim-wizard-mentor-training-step-hours-to-claim-maximum-field").checked?
end
def then_i_see_the_error(message)
diff --git a/spec/system/claims/support/schools/claims/create_claim_spec.rb b/spec/system/claims/support/schools/claims/create_claim_spec.rb
index 39a53a428..8d9165c62 100644
--- a/spec/system/claims/support/schools/claims/create_claim_spec.rb
+++ b/spec/system/claims/support/schools/claims/create_claim_spec.rb
@@ -15,7 +15,6 @@
create(
:claims_support_user,
:colin,
- user_memberships: [create(:user_membership, organisation: school)],
)
end
let!(:bpn) { create(:claims_provider, :best_practice_network) }
@@ -146,12 +145,12 @@
when_i_click("Change Accredited provider")
when_i_choose_a_provider(bpn)
when_i_click("Continue")
- when_i_click("Change Mentors")
then_i_should_see_the_message("There are no mentors you can include in a claim because they have already had 20 hours of training claimed for with Best Practice Network.")
when_i_click("Change the accredited provider")
when_i_choose_a_provider(niot)
when_i_click("Continue")
when_i_click("Continue")
+ when_i_click("Continue")
then_i_should_land_on_the_check_page
end
@@ -247,9 +246,9 @@ def then_i_check_my_answers
end
end
- def then_i_expect_the_training_hours_for(hours, mentor)
+ def then_i_expect_the_training_hours_for(_hours, mentor)
expect(page).to have_content("Hours of training for #{mentor.full_name}")
- find("#claims-support-claim-mentor-training-form-hours-completed-#{hours}-field").checked?
+ find("#claims-add-claim-wizard-mentor-training-step-hours-to-claim-maximum-field").checked?
end
def then_i_am_redirectd_to_index_page(claim)
diff --git a/spec/wizards/claims/add_claim_wizard/mentor_step_spec.rb b/spec/wizards/claims/add_claim_wizard/mentor_step_spec.rb
new file mode 100644
index 000000000..ef8ca3346
--- /dev/null
+++ b/spec/wizards/claims/add_claim_wizard/mentor_step_spec.rb
@@ -0,0 +1,93 @@
+require "rails_helper"
+
+RSpec.describe Claims::AddClaimWizard::MentorStep, type: :model do
+ subject(:step) { described_class.new(wizard: mock_wizard, attributes:) }
+
+ let(:mock_wizard) do
+ instance_double(Claims::AddClaimWizard).tap do |mock_wizard|
+ allow(mock_wizard).to receive_messages(school:, mentors_with_claimable_hours: claimable_mentors)
+ end
+ end
+ let(:school) { create(:claims_school) }
+ let!(:mentor_1) { create(:claims_mentor, schools: [school]) }
+ let!(:mentor_2) { create(:claims_mentor, schools: [school]) }
+ let(:claimable_mentors) do
+ Claims::Mentor.where(id: [mentor_1.id, mentor_2.id])
+ end
+
+ let(:attributes) { nil }
+
+ describe "attributes" do
+ it { is_expected.to have_attributes(mentor_ids: []) }
+ end
+
+ describe "validations" do
+ it { is_expected.to validate_inclusion_of(:mentor_ids).in_array(claimable_mentors.ids) }
+ end
+
+ describe "delegations" do
+ it { is_expected.to delegate_method(:school).to(:wizard) }
+ it { is_expected.to delegate_method(:claim).to(:wizard) }
+ it { is_expected.to delegate_method(:mentors_with_claimable_hours).to(:wizard) }
+ end
+
+ describe "#selected_mentors" do
+ subject(:selected_mentors) { step.selected_mentors }
+
+ let(:attributes) { { mentor_ids: [mentor_1.id, mentor_2.id] } }
+
+ context "when there are no mentors with claimable hours with the provider" do
+ let(:claimable_mentors) { nil }
+
+ it "returns no mentors" do
+ expect(selected_mentors).to eq([])
+ end
+ end
+
+ context "when there are mentors with claimable hours with the provider" do
+ let(:claimable_mentors) { Claims::Mentor.where(id: mentor_1.id) }
+
+ it "returns all selected mentors" do
+ expect(selected_mentors).to contain_exactly(mentor_1, mentor_2)
+ end
+ end
+ end
+
+ describe "#all_school_mentors_visible?" do
+ subject(:all_school_mentors_visible) { step.all_school_mentors_visible? }
+
+ context "when the number of members assigned to a school is the same as the number of mentors with claimable hours" do
+ it "returns true" do
+ expect(all_school_mentors_visible).to be(true)
+ end
+ end
+
+ context "when the number of members assigned to a school is not the same as the number of mentors with claimable hours" do
+ let(:claimable_mentors) do
+ Claims::Mentor.where(id: mentor_1.id)
+ end
+
+ it "returns true" do
+ expect(all_school_mentors_visible).to be(false)
+ end
+ end
+ end
+
+ describe "#mentor_ids=" do
+ context "when the value is blank" do
+ it "remains blank" do
+ step.mentor_ids = []
+
+ expect(step.mentor_ids).to eq([])
+ end
+ end
+
+ context "when the value includes nil" do
+ it "removes all values except valid mentor ids" do
+ step.mentor_ids = [nil, mentor_1.id, mentor_2.id]
+
+ expect(step.mentor_ids).to contain_exactly(mentor_1.id, mentor_2.id)
+ end
+ end
+ end
+end
diff --git a/spec/wizards/claims/add_claim_wizard/mentor_training_step_spec.rb b/spec/wizards/claims/add_claim_wizard/mentor_training_step_spec.rb
new file mode 100644
index 000000000..fbbfd0a58
--- /dev/null
+++ b/spec/wizards/claims/add_claim_wizard/mentor_training_step_spec.rb
@@ -0,0 +1,139 @@
+require "rails_helper"
+
+RSpec.describe Claims::AddClaimWizard::MentorTrainingStep, type: :model do
+ subject(:step) { described_class.new(wizard: mock_wizard, attributes:) }
+
+ let(:mock_wizard) do
+ instance_double(Claims::AddClaimWizard).tap do |mock_wizard|
+ allow(mock_wizard).to receive_messages(
+ school:,
+ provider:,
+ academic_year: claim_window.academic_year,
+ steps: { mentor: mentor_step },
+ )
+ end
+ end
+ let(:mentor_step) do
+ instance_double(Claims::AddClaimWizard::MentorStep).tap do |mentor_step|
+ allow(mentor_step).to receive(:mentor_ids).and_return([mentor.id])
+ allow(mentor_step).to receive_messages(
+ mentor_ids: [mentor.id],
+ selected_mentors: Claims::Mentor.where(id: mentor.id),
+ )
+ end
+ end
+ let(:school) { create(:claims_school) }
+ let(:provider) { create(:claims_provider) }
+ let!(:mentor) { create(:claims_mentor, schools: [school]) }
+ let(:claim_window) { Claims::ClaimWindow.current || create(:claim_window, :current) }
+ let(:attributes) { nil }
+
+ describe "attributes" do
+ it { is_expected.to have_attributes(mentor_id: nil, hours_to_claim: nil, custom_hours: nil) }
+ end
+
+ describe "validations" do
+ it { is_expected.to validate_presence_of(:mentor_id) }
+ it { is_expected.to validate_presence_of(:hours_to_claim) }
+ it { is_expected.to validate_inclusion_of(:hours_to_claim).in_array(described_class::HOURS_TO_CLAIM) }
+
+ context "when custom are selected" do
+ let(:attributes) { { hours_to_claim: "custom", mentor_id: mentor.id } }
+
+ it do
+ expect(step).to validate_numericality_of(:custom_hours)
+ .only_integer
+ .is_greater_than_or_equal_to(1)
+ .is_less_than_or_equal_to(20)
+ .with_message("Enter the number of hours between 1 and 20")
+ end
+ end
+
+ context "when custom are not selected" do
+ let(:attributes) { { hours_to_claim: "maximum", mentor_id: mentor.id } }
+
+ it { is_expected.to be_valid }
+ it { is_expected.not_to validate_presence_of(:custom_hours) }
+ end
+
+ describe "delegations" do
+ it { is_expected.to delegate_method(:provider).to(:wizard) }
+ it { is_expected.to delegate_method(:name).to(:provider).with_prefix(true) }
+ it { is_expected.to delegate_method(:full_name).to(:mentor).with_prefix(true) }
+ end
+ end
+
+ describe "#mentor" do
+ context "when a mentor id is given" do
+ let(:attributes) { { mentor_id: mentor.id } }
+
+ it "return the mentor associated with the given mentor id" do
+ expect(step.mentor).to eq(mentor)
+ end
+ end
+
+ context "when a mentor id is not given" do
+ it "return the mentor associated with the given mentor id" do
+ expect(step.mentor).to be_nil
+ end
+ end
+ end
+
+ describe "#training_allowance" do
+ subject(:training_allowance) { step.training_allowance }
+
+ let(:attributes) { { mentor_id: mentor.id } }
+
+ it "returns the training allowance for a given mentor" do
+ expect(training_allowance).to be_a(Claims::TrainingAllowance)
+ end
+ end
+
+ describe "#max_hours" do
+ subject(:max_hours) { step.max_hours }
+
+ let(:attributes) { { mentor_id: mentor.id } }
+
+ context "when the mentor has no previous training hours with the provider" do
+ it "returns the maximum number of hours" do
+ expect(max_hours).to eq(20)
+ end
+ end
+
+ context "when the mentor has previous training hours with the provider" do
+ before do
+ existing_claim = create(:claim, :submitted, provider:, school:, claim_window:)
+ create(:mentor_training,
+ claim: existing_claim,
+ hours_completed: 1,
+ mentor:,
+ provider:,
+ date_completed: claim_window.starts_on)
+ end
+
+ it "returns the remaining number of claimable hours" do
+ expect(max_hours).to eq(19)
+ end
+ end
+ end
+
+ describe "#hours_completed" do
+ subject(:hours_completed) { step.hours_completed }
+
+ context "when custom hours completed is present" do
+ let(:attributes) { { mentor_id: mentor.id, custom_hours: 6, hours_to_claim: "custom" } }
+
+ it "returns hours completed" do
+ expect(hours_completed).to eq(6)
+ end
+ end
+
+ context "when custom hours completed is not present" do
+ let(:attributes) { { mentor_id: mentor.id, hours_to_claim: "maximum", custom_hours: 6 } }
+
+ it "returns hours completed" do
+ expect(hours_completed).to eq(20)
+ end
+ end
+ end
+end
diff --git a/spec/wizards/claims/add_claim_wizard/provider_step_spec.rb b/spec/wizards/claims/add_claim_wizard/provider_step_spec.rb
new file mode 100644
index 000000000..a73bdce51
--- /dev/null
+++ b/spec/wizards/claims/add_claim_wizard/provider_step_spec.rb
@@ -0,0 +1,56 @@
+require "rails_helper"
+
+RSpec.describe Claims::AddClaimWizard::ProviderStep, type: :model do
+ subject(:step) { described_class.new(wizard: mock_wizard, attributes:) }
+
+ let(:attributes) { nil }
+ let!(:niot_provider) { create(:claims_provider, :niot) }
+ let!(:bpn_provider) { create(:claims_provider, :best_practice_network) }
+
+ let(:mock_wizard) do
+ instance_double(Claims::AddClaimWizard)
+ end
+
+ describe "attributes" do
+ it { is_expected.to have_attributes(id: nil) }
+ end
+
+ describe "validations" do
+ it { is_expected.to validate_inclusion_of(:id).in_array([niot_provider.id, bpn_provider.id]) }
+ end
+
+ describe "#providers_for_selection" do
+ subject(:providers_for_selection) { step.providers_for_selection }
+
+ before { create(:claims_provider) }
+
+ it "returns only the providers scoped in the private beta" do
+ private_beta_providers = Claims::Provider.where(id: [bpn_provider.id, niot_provider.id])
+ expect(providers_for_selection).to match_array(private_beta_providers.select(:id, :name))
+ end
+ end
+
+ describe "#provider" do
+ subject { step.provider }
+
+ context "when id is set" do
+ context "when the provider is a private beta provider" do
+ let(:attributes) { { id: niot_provider.id } }
+
+ it { is_expected.to eq(niot_provider) }
+ end
+
+ context "when the provider is not a private beta provider" do
+ let(:attributes) { { id: create(:claims_provider).id } }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context "when id is nil" do
+ let(:attributes) { { id: nil } }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/wizards/claims/add_claim_wizard_spec.rb b/spec/wizards/claims/add_claim_wizard_spec.rb
new file mode 100644
index 000000000..2e0abe874
--- /dev/null
+++ b/spec/wizards/claims/add_claim_wizard_spec.rb
@@ -0,0 +1,268 @@
+require "rails_helper"
+
+RSpec.describe Claims::AddClaimWizard do
+ subject(:wizard) { described_class.new(school:, created_by:, state:, params:, current_step: nil) }
+
+ let(:state) { {} }
+ let(:params_data) { {} }
+ let(:params) { ActionController::Parameters.new(params_data) }
+ let(:school) { create(:claims_school) }
+ let(:created_by) { create(:claims_user, schools: [school]) }
+ let(:provider) { create(:claims_provider, :niot) }
+ let(:claim_window) { Claims::ClaimWindow.current || create(:claim_window, :current) }
+
+ before { claim_window }
+
+ describe "#steps" do
+ subject { wizard.steps.keys }
+
+ let(:state) do
+ {
+ "provider" => { "id" => provider.id },
+ }
+ end
+
+ context "when the school has no mentors" do
+ it { is_expected.to eq %i[provider no_mentors] }
+ end
+
+ context "when the school has mentors" do
+ let!(:mentor_1) { create(:claims_mentor, schools: [school], first_name: "Alan", last_name: "Anderson") }
+
+ context "with claimable hours" do
+ it { is_expected.to eq %i[provider mentor check_your_answers] }
+ end
+
+ context "when mentors have been selected" do
+ let!(:mentor_2) { create(:claims_mentor, schools: [school], first_name: "Bob", last_name: "Bletcher") }
+
+ let(:state) do
+ {
+ "provider" => { "id" => provider.id },
+ "mentor" => { "mentor_ids" => [mentor_1.id, mentor_2.id] },
+ }
+ end
+
+ it { is_expected.to eq [:provider, :mentor, "mentor_training_#{mentor_1.id}".to_sym, "mentor_training_#{mentor_2.id}".to_sym, :check_your_answers] }
+ end
+
+ context "with no claimable hours" do
+ before do
+ create(:mentor_training,
+ hours_completed: 20,
+ mentor: mentor_1,
+ provider:,
+ date_completed: claim_window.starts_on + 1.day,
+ claim: create(:claim, :submitted, school:, provider:))
+ end
+
+ it { is_expected.to eq %i[provider no_mentors] }
+ end
+ end
+ end
+
+ describe "#add_step" do
+ # this methods behaves just as it does in the BaseWizard,
+ # unless preset attributes are given.
+ context "when preset attribute 'mentor_id' is given" do
+ let(:mentor_id) { "abcd" }
+
+ it "adds a step, with the 'mentor_id' step name and attributes" do
+ wizard.add_step(Claims::AddClaimWizard::MentorTrainingStep, { mentor_id: })
+ expect(wizard.steps).to include(:mentor_training_abcd)
+ expect(wizard.steps[:mentor_training_abcd]).to be_a(Claims::AddClaimWizard::MentorTrainingStep)
+ expect(wizard.steps[:mentor_training_abcd]).to have_attributes(mentor_id:)
+ end
+ end
+ end
+
+ describe "#academic_year" do
+ before { claim_window }
+
+ it "returns the academic year of the current claim window" do
+ expect(wizard.academic_year).to eq(claim_window.academic_year)
+ end
+ end
+
+ describe "#total_hours" do
+ let(:mentor_1) { create(:claims_mentor, schools: [school]) }
+ let(:mentor_2) { create(:claims_mentor, schools: [school]) }
+ let(:mentor_3) { create(:claims_mentor, schools: [school]) }
+ let(:mentor_4) { create(:claims_mentor, schools: [school]) }
+ let(:state) do
+ {
+ "provider" => { "id" => provider.id },
+ "mentor" => { "mentor_ids" => [mentor_1.id, mentor_2.id, mentor_3.id, mentor_4.id] },
+ "mentor_training_#{mentor_1.id}" => {
+ "mentor_id" => mentor_1.id, "hours_to_claim" => "maximum"
+ },
+ "mentor_training_#{mentor_2.id}" => {
+ "mentor_id" => mentor_2.id, "hours_to_claim" => "maximum"
+ },
+ "mentor_training_#{mentor_3.id}" => {
+ "mentor_id" => mentor_3.id, "hours_to_claim" => "custom", "custom_hours" => 16
+ },
+ "mentor_training_#{mentor_4.id}" => {
+ "mentor_id" => mentor_4.id, "hours_to_claim" => "custom", "custom_hours" => 4
+ },
+ }
+ end
+
+ it "return the sum of the hours completed and custom hours completed" do
+ expect(wizard.total_hours).to eq(60)
+ end
+ end
+
+ describe "#claim" do
+ let(:mentor_1) { create(:claims_mentor, schools: [school]) }
+ let(:mentor_2) { create(:claims_mentor, schools: [school]) }
+ let(:state) do
+ {
+ "provider" => { "id" => provider.id },
+ "mentor" => { "mentor_ids" => [mentor_1.id, mentor_2.id] },
+ "mentor_training_#{mentor_1.id}" => {
+ "mentor_id" => mentor_1.id, "hours_to_claim" => "maximum"
+ },
+ "mentor_training_#{mentor_2.id}" => {
+ "mentor_id" => mentor_2.id, "hours_to_claim" => "custom", "custom_hours" => 16
+ },
+ }
+ end
+
+ it "initialises a new claim, build from the step attributes" do
+ claim = wizard.claim
+ expect(claim.new_record?).to be(true)
+ expect(claim).to be_a(Claims::Claim)
+ expect(claim.provider).to eq(provider)
+ expect(claim.school).to eq(school)
+ expect(claim.created_by).to eq(created_by)
+ end
+
+ it "initialises new mentor trainings for the claim, per mentor set in the step attributes" do
+ claim = wizard.claim
+ expect(claim.mentor_trainings.size).to eq(2)
+ expect(claim.mentor_trainings.map(&:mentor)).to contain_exactly(mentor_1, mentor_2)
+ expect(claim.mentor_trainings.map(&:hours_completed)).to contain_exactly(20, 16)
+ end
+ end
+
+ describe "#create_claim" do
+ subject(:create_claim) { wizard.create_claim }
+
+ let(:mentor_1) { create(:claims_mentor, schools: [school], first_name: "Alan", last_name: "Anderson") }
+ let(:mentor_2) { create(:claims_mentor, schools: [school], first_name: "Bob", last_name: "Bletcher") }
+
+ let(:state) do
+ {
+ "provider" => { "id" => provider.id },
+ "mentor" => { "mentor_ids" => [mentor_1.id, mentor_2.id] },
+ "mentor_training_#{mentor_1.id}" => {
+ "mentor_id" => mentor_1.id, "hours_to_claim" => "maximum"
+ },
+ "mentor_training_#{mentor_2.id}" => {
+ "mentor_id" => mentor_2.id, "hours_to_claim" => "custom", "custom_hours" => 16
+ },
+ }
+ end
+
+ context "when the mentors still have available training hours with the provider" do
+ context "when the created by user, is not a support user" do
+ it "creates a submitted claim" do
+ expect { create_claim }.to change(Claims::Claim, :count).by(1)
+ .and change(Claims::MentorTraining, :count).by(2)
+
+ claim = wizard.claim
+
+ expect(claim).to be_persisted
+ expect(claim.school).to eq(school)
+ expect(claim.provider).to eq(provider)
+ expect(claim.created_by).to eq(created_by)
+ expect(claim.status).to eq("submitted")
+ expect(claim.claim_window).to eq(claim_window)
+ expect(claim.mentors.order_by_full_name).to contain_exactly(mentor_1, mentor_2)
+
+ mentor_1_training = claim.mentor_trainings.find_by(mentor_id: mentor_1)
+ expect(mentor_1_training.hours_completed).to eq(20)
+ expect(mentor_1_training.provider).to eq(provider)
+
+ mentor_2_training = claim.mentor_trainings.find_by(mentor_id: mentor_2)
+ expect(mentor_2_training.hours_completed).to eq(16)
+ expect(mentor_2_training.provider).to eq(provider)
+ end
+ end
+
+ context "when the created by user, is a support user" do
+ let(:created_by) { create(:claims_support_user) }
+
+ it "creates a draft claim" do
+ expect { create_claim }.to change(Claims::Claim, :count).by(1)
+ .and change(Claims::MentorTraining, :count).by(2)
+
+ claim = wizard.claim
+
+ expect(claim).to be_persisted
+ expect(claim.school).to eq(school)
+ expect(claim.provider).to eq(provider)
+ expect(claim.created_by).to eq(created_by)
+ expect(claim.status).to eq("draft")
+ expect(claim.claim_window).to eq(claim_window)
+ expect(claim.mentors.order_by_full_name).to contain_exactly(mentor_1, mentor_2)
+
+ mentor_1_training = claim.mentor_trainings.find_by(mentor_id: mentor_1)
+ expect(mentor_1_training.hours_completed).to eq(20)
+ expect(mentor_1_training.provider).to eq(provider)
+
+ mentor_2_training = claim.mentor_trainings.find_by(mentor_id: mentor_2)
+ expect(mentor_2_training.hours_completed).to eq(16)
+ expect(mentor_2_training.provider).to eq(provider)
+ end
+ end
+ end
+ end
+
+ describe "#mentors_with_claimable_hours" do
+ subject(:mentors_with_claimable_hours) { wizard.mentors_with_claimable_hours }
+
+ context "when a provider is not provided" do
+ it "returns no mentors" do
+ expect(mentors_with_claimable_hours).to eq([])
+ end
+ end
+
+ context "when a provider is provided" do
+ let(:mentor_1) { create(:claims_mentor, schools: [school]) }
+ let!(:mentor_2) { create(:claims_mentor, schools: [school]) }
+ let(:state) do
+ {
+ "provider" => { "id" => provider.id },
+ }
+ end
+
+ before do
+ existing_claim = create(:claim, :submitted, provider:, school:, claim_window:)
+ create(:mentor_training,
+ claim: existing_claim,
+ hours_completed: 20,
+ mentor: mentor_1,
+ provider:,
+ date_completed: claim_window.starts_on)
+ end
+
+ it "returns all mentors with available hours with the provider" do
+ expect(mentors_with_claimable_hours).to contain_exactly(mentor_2)
+ end
+ end
+ end
+
+ describe "#provider" do
+ let(:state) do
+ {
+ "provider" => { "id" => provider.id },
+ }
+ end
+
+ it "returns the provider given by the provider step" do
+ expect(wizard.provider).to eq(provider)
+ end
+ end
+end