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 %> + + <% 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") %>

+ + + + <%= 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