From 00afa3f34887ca93a31be1d61c144cf8b17b20cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leandro=20Alem=C3=A3o?= Date: Fri, 31 Jan 2025 09:29:47 +0000 Subject: [PATCH] [CPDLP-3937] Add mentor funding statement and output calculators --- .../finance/statements/ecf_details_table.rb | 2 +- .../finance/ecf/statements_controller.rb | 6 +- app/models/finance/statement/ecf.rb | 2 +- .../finance/ecf/ect/output_calculator.rb | 250 ++++++++++++++ .../finance/ecf/ect/statement_calculator.rb | 200 +++++++++++ .../finance/ecf/mentor/output_calculator.rb | 71 ++++ .../ecf/mentor/statement_calculator.rb | 83 +++++ app/services/finance/ecf/output_calculator.rb | 241 +------------ .../finance/ecf/statement_calculator.rb | 191 +---------- .../finance/ecf/statements/show.html.erb | 137 ++++++++ .../finance/payment_breakdown_spec.rb | 2 +- ...d_declaration_in_payment_breakdown_spec.rb | 2 +- .../ecf/{ => ect}/output_calculator_spec.rb | 2 +- .../{ => ect}/statement_calculator_spec.rb | 112 ++++-- .../ecf/mentor/output_calculator_spec.rb | 323 ++++++++++++++++++ .../ecf/mentor/statement_calculator_spec.rb | 165 +++++++++ 16 files changed, 1323 insertions(+), 466 deletions(-) create mode 100644 app/services/finance/ecf/ect/output_calculator.rb create mode 100644 app/services/finance/ecf/ect/statement_calculator.rb create mode 100644 app/services/finance/ecf/mentor/output_calculator.rb create mode 100644 app/services/finance/ecf/mentor/statement_calculator.rb rename spec/services/finance/ecf/{ => ect}/output_calculator_spec.rb (99%) rename spec/services/finance/ecf/{ => ect}/statement_calculator_spec.rb (86%) create mode 100644 spec/services/finance/ecf/mentor/output_calculator_spec.rb create mode 100644 spec/services/finance/ecf/mentor/statement_calculator_spec.rb diff --git a/app/components/finance/statements/ecf_details_table.rb b/app/components/finance/statements/ecf_details_table.rb index 0cbafc52fb..dfef1e78f2 100644 --- a/app/components/finance/statements/ecf_details_table.rb +++ b/app/components/finance/statements/ecf_details_table.rb @@ -9,7 +9,7 @@ class ECFDetailsTable < BaseComponent def initialize(statement:) @statement = statement - @calculator = Finance::ECF::StatementCalculator.new(statement: @statement) + @calculator = Finance::ECF::ECT::StatementCalculator.new(statement: @statement) end end end diff --git a/app/controllers/finance/ecf/statements_controller.rb b/app/controllers/finance/ecf/statements_controller.rb index 5b34785db1..f3e3fb3d11 100644 --- a/app/controllers/finance/ecf/statements_controller.rb +++ b/app/controllers/finance/ecf/statements_controller.rb @@ -6,8 +6,12 @@ class StatementsController < BaseController def show @ecf_lead_provider = lead_provider_scope.find(params[:payment_breakdown_id]) @statement = @ecf_lead_provider.statements.find(params[:id]) - @calculator = StatementCalculator.new(statement: @statement) + @calculator = ECT::StatementCalculator.new(statement: @statement) set_important_message(title: t("finance.statements.payment_authorisations.banner.title"), content: t("finance.statements.payment_authorisations.banner.content", statement_marked_as_paid_at: @statement.marked_as_paid_at.strftime("%-I:%M%P on %-e %b %Y"))) if authorising_for_payment_banner_visible?(@statement) + + if @statement.cohort.mentor_funding? + @mentor_calculator = Mentor::StatementCalculator.new(statement: @statement) + end end private diff --git a/app/models/finance/statement/ecf.rb b/app/models/finance/statement/ecf.rb index 8a4ed9c112..cf3969aae5 100644 --- a/app/models/finance/statement/ecf.rb +++ b/app/models/finance/statement/ecf.rb @@ -30,7 +30,7 @@ def paid! end def calculator - @calculator ||= Finance::ECF::StatementCalculator.new(statement: self) + @calculator ||= Finance::ECF::ECT::StatementCalculator.new(statement: self) end def previous_statements diff --git a/app/services/finance/ecf/ect/output_calculator.rb b/app/services/finance/ecf/ect/output_calculator.rb new file mode 100644 index 0000000000..5e88dfe35a --- /dev/null +++ b/app/services/finance/ecf/ect/output_calculator.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +module Finance + module ECF + module ECT + class OutputCalculator < Finance::ECF::OutputCalculator + COHORT_WITH_MENTOR_FUNDING_DECLARATION_CLASS_TYPES = ["ParticipantDeclaration::ECT"].freeze + + def banding_breakdown + @banding_breakdown ||= begin + bandings = declaration_types.map do |declaration_type| + current_banding_for_declaration_type(declaration_type) + end + + result = bandings[0] + + band_letters.each do |letter| + bandings[1..].each do |banding| + new_chunk = banding.find { |e| e[:band] == letter } + result.find { |e| e[:band] == letter }.merge!(new_chunk) + end + end + + result + end + end + + def uplift_breakdown + @uplift_breakdown ||= { + previous_count: previous_fill_level_for_uplift, + count: current_billable_count_for_uplift - current_refundable_count_for_uplift, + additions: current_billable_count_for_uplift, + subtractions: current_refundable_count_for_uplift, + } + end + + def fee_for_declaration(band_letter:, type:) + percentage = case type + when :started + started_event_percentage + when :completed + completed_event_percentage + when :retained_1, :retained_2, :retained_3, :retained_4 + retained_event_percentage + when :extended_1, :extended_2, :extended_3 + extended_event_percentage + end + + percentage * band_for_letter(band_letter).output_payment_per_participant + end + + private + + def band_for_letter(letter) + bands.zip(:a..:z).find { |e| e[1] == letter }[0] + end + + def started_event_percentage + 0.2 + end + + def completed_event_percentage + 0.2 + end + + def retained_event_percentage + 0.15 + end + + def extended_event_percentage + 0.15 + end + + def declaration_types + %w[ + started + retained-1 + retained-2 + retained-3 + retained-4 + completed + extended-1 + extended-2 + extended-3 + ] + end + + # this is a 3 pass algorithm + # first pass adds billable declarations + # second pass subtracts refunds + # third pass further subtracts refunds if statement is net negative + def current_banding_for_declaration_type(declaration_type) + pot_size = current_billable_count_for_declaration_type(declaration_type) + + banding = previous_banding_for_declaration_type(declaration_type).map do |hash| + band_capacity = hash[:max] - (hash[:min] - 1) - hash[:"previous_#{declaration_type.underscore}_count"] + + fill_level = [pot_size, band_capacity].min + + pot_size -= fill_level + + hash[:"#{declaration_type.underscore}_count"] = fill_level + hash[:"#{declaration_type.underscore}_additions"] = fill_level + + hash + end + + pot_size = current_refundable_count_declaration_type(declaration_type) + + banding = banding.reverse.map do |hash| + fill_level = hash[:"#{declaration_type.underscore}_count"] + + available = [fill_level, pot_size].min + + hash[:"#{declaration_type.underscore}_count"] = fill_level - available + hash[:"#{declaration_type.underscore}_subtractions"] = available + + pot_size -= available + + hash + end + + if pot_size.positive? + banding = banding.map do |hash| + available = hash[:"previous_#{declaration_type.underscore}_count"] + + unless available.zero? + reduction = [pot_size, available].min + + hash[:"#{declaration_type.underscore}_count"] = -reduction + hash[:"#{declaration_type.underscore}_subtractions"] += reduction + + pot_size -= reduction + end + + hash + end + end + + banding.reverse + end + + def previous_banding_for_declaration_type(declaration_type) + pot_size = previous_fill_level_for_declaration_type(declaration_type) + + bands.zip(:a..:z).map do |band, letter| + # minimum band should always be 1 or more, otherwise band a will go over + # its max limit + band_min = band.min.to_i.zero? ? 1 : band.min + + band_capacity = band.max - band_min + 1 + + fill_level = [pot_size, band_capacity].min + + pot_size -= fill_level + + key_name = "previous_#{declaration_type.underscore}_count".to_sym + + { + band: letter, + min: band_min, + max: band.max, + key_name => fill_level, + } + end + end + + def bands + statement.contract.bands.order(max: :asc) + end + + def band_letters + bands.zip(:a..:z).map { |e| e[1] } + end + + def previous_fill_level_for_declaration_type(declaration_type) + billable = Finance::StatementLineItem + .where(statement: statement.previous_statements) + .billable + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type:, type: participant_declaration_class_types }) + .count + + refundable = Finance::StatementLineItem + .where(statement: statement.previous_statements) + .refundable + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type:, type: participant_declaration_class_types }) + .count + + billable - refundable + end + + def previous_fill_level_for_uplift + billable = Finance::StatementLineItem + .where(statement: statement.previous_statements) + .billable + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type: "started", type: participant_declaration_class_types }) + .where("participant_declarations.sparsity_uplift = true OR participant_declarations.pupil_premium_uplift = true") + .count + + refundable = Finance::StatementLineItem + .where(statement: statement.previous_statements) + .refundable + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type: "started", type: participant_declaration_class_types }) + .where("participant_declarations.sparsity_uplift = true OR participant_declarations.pupil_premium_uplift = true") + .count + + billable - refundable + end + + def current_billable_count_for_declaration_type(declaration_type) + statement + .billable_statement_line_items + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type:, type: participant_declaration_class_types }) + .count + end + + def current_refundable_count_declaration_type(declaration_type) + statement + .refundable_statement_line_items + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type:, type: participant_declaration_class_types }) + .count + end + + def current_billable_count_for_uplift + statement + .billable_statement_line_items + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type: "started", type: participant_declaration_class_types }) + .where("participant_declarations.sparsity_uplift = true OR participant_declarations.pupil_premium_uplift = true") + .count + end + + def current_refundable_count_for_uplift + statement + .refundable_statement_line_items + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type: "started", type: participant_declaration_class_types }) + .where("participant_declarations.sparsity_uplift = true OR participant_declarations.pupil_premium_uplift = true") + .count + end + end + end + end +end diff --git a/app/services/finance/ecf/ect/statement_calculator.rb b/app/services/finance/ecf/ect/statement_calculator.rb new file mode 100644 index 0000000000..eabe7950ab --- /dev/null +++ b/app/services/finance/ecf/ect/statement_calculator.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +require "payment_calculator/ecf/service_fees" + +module Finance + module ECF + module ECT + class StatementCalculator < Finance::ECF::StatementCalculator + def self.event_types + %i[ + started + retained_1 + retained_2 + retained_3 + retained_4 + completed + extended_1 + extended_2 + extended_3 + ] + end + + def self.event_types_for_display + %i[ + started + retained_1 + retained_2 + retained_3 + retained_4 + completed + ] + end + + def self.band_mapping + { + a: 0, + b: 1, + c: 2, + d: 3, + } + end + + def bands + @bands ||= statement.contract.bands.order(max: :asc) + end + + def band_letters + (:a..:z).take(bands.size) + end + + event_types.each do |event_type| + band_mapping.each_key do |letter| + define_method "#{event_type}_band_#{letter}_count" do + output_calculator.banding_breakdown.find { |e| e[:band] == letter }[:"#{event_type}_count"] + end + + define_method "#{event_type}_band_#{letter}_additions" do + output_calculator.banding_breakdown.find { |e| e[:band] == letter }[:"#{event_type}_additions"] + end + + define_method "#{event_type}_band_#{letter}_fee_per_declaration" do + output_calculator.fee_for_declaration(band_letter: letter, type: event_type) + end + end + + define_method "additions_for_#{event_type}" do + output_calculator.banding_breakdown.sum do |hash| + hash[:"#{event_type}_additions"] * output_calculator.fee_for_declaration(band_letter: hash[:band], type: event_type) + end + end + + define_method "deductions_for_#{event_type}" do + output_calculator.banding_breakdown.sum do |hash| + hash[:"#{event_type}_subtractions"] * output_calculator.fee_for_declaration(band_letter: hash[:band], type: event_type) + end + end + end + + band_mapping.each_key do |letter| + define_method "extended_band_#{letter}_additions" do + send("extended_1_band_#{letter}_additions") + + send("extended_2_band_#{letter}_additions") + + send("extended_3_band_#{letter}_additions") + end + + define_method "extended_band_#{letter}_fee_per_declaration" do + send("extended_1_band_#{letter}_fee_per_declaration") + end + end + + def additions_for_extended + additions_for_extended_1 + additions_for_extended_2 + additions_for_extended_3 + end + + def fee_for_declaration(band_letter:, type:) + output_calculator.fee_for_declaration(band_letter:, type:) + end + + def started_count + output_calculator.banding_breakdown.sum do |hash| + hash[:started_additions] + end + end + + def retained_count + output_calculator.banding_breakdown.sum do |hash| + hash.select { |k, _| k.match(/retained_\d_additions/) }.values.sum + end + end + + def completed_count + output_calculator.banding_breakdown.sum do |hash| + hash[:completed_additions] + end + end + + def extended_count + output_calculator.banding_breakdown.sum do |hash| + hash.select { |k, _| k.match(/extended_\d_additions/) }.values.sum + end + end + + def uplift_count + output_calculator.uplift_breakdown[:count] + end + + def uplift_additions_count + return 0.0 unless statement.contract.include_uplift_fees? + + output_calculator.uplift_breakdown[:additions] + end + + def uplift_deductions_count + return 0 unless statement.contract.include_uplift_fees? + + output_calculator.uplift_breakdown[:subtractions] + end + + def uplift_fee_per_declaration + return 0.0 unless statement.contract.include_uplift_fees? + + statement.contract.uplift_amount + end + + def total_for_uplift + return 0.0 unless statement.contract.include_uplift_fees? + + previous_uplift_count = output_calculator.uplift_breakdown[:previous_count] + previous_uplift_amount = previous_uplift_count * uplift_fee_per_declaration + + # uplift_clawback_deductions is a negative number so doing a double negative -- + # we're adding it back as we had subtracted from adjustments_total + delta_uplift_amount = uplift_count * uplift_fee_per_declaration - uplift_clawback_deductions + + available = [(statement.contract.uplift_cap - previous_uplift_amount), 0].max + + [available, delta_uplift_amount].min + end + + def uplift_clawback_deductions + uplift_deductions_count * -uplift_fee_per_declaration + end + + def adjustments_total + -clawback_deductions + uplift_clawback_deductions + end + + def total(with_vat: false) + sum = service_fee + output_fee + total_for_uplift + adjustments_total + additional_adjustments_total + statement.reconcile_amount + sum += vat if with_vat + sum + end + + def service_fee + contract.monthly_service_fee || calculated_service_fee + end + + def event_types_for_display + self.class.event_types_for_display.tap do |types| + types << :extended if extended_count.positive? + end + end + + private + + def output_calculator + @output_calculator ||= OutputCalculator.new(statement:) + end + + def calculated_service_fee + PaymentCalculator::ECF::ServiceFees.new(contract:).call.sum { |hash| hash[:monthly] } + end + + def band_mapping + self.class.band_mapping + end + end + end + end +end diff --git a/app/services/finance/ecf/mentor/output_calculator.rb b/app/services/finance/ecf/mentor/output_calculator.rb new file mode 100644 index 0000000000..6853060e4f --- /dev/null +++ b/app/services/finance/ecf/mentor/output_calculator.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Finance + module ECF + module Mentor + class OutputCalculator < Finance::ECF::OutputCalculator + COHORT_WITH_MENTOR_FUNDING_DECLARATION_CLASS_TYPES = ["ParticipantDeclaration::Mentor"].freeze + + def output_breakdown + @output_breakdown ||= declaration_types.map do |declaration_type| + current_output_for_declaration_type(declaration_type) + end + end + + def fee_for_declaration(type:) + percentage = case type + when :started + started_event_percentage + when :completed + completed_event_percentage + end + + percentage * statement.mentor_contract.payment_per_participant + end + + private + + def started_event_percentage + 0.5 + end + + def completed_event_percentage + 0.5 + end + + def declaration_types + %w[ + started + completed + ] + end + + def current_output_for_declaration_type(declaration_type) + current_output_count = current_billable_count_for_declaration_type(declaration_type) + refundable_output_count = current_refundable_count_declaration_type(declaration_type) + + hash = {} + hash[:"#{declaration_type.underscore}_additions"] = current_output_count + hash[:"#{declaration_type.underscore}_subtractions"] = refundable_output_count + hash + end + + def current_billable_count_for_declaration_type(declaration_type) + statement + .billable_statement_line_items + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type:, type: "ParticipantDeclaration::Mentor" }) + .count + end + + def current_refundable_count_declaration_type(declaration_type) + statement + .refundable_statement_line_items + .joins(:participant_declaration) + .where(participant_declarations: { declaration_type:, type: "ParticipantDeclaration::Mentor" }) + .count + end + end + end + end +end diff --git a/app/services/finance/ecf/mentor/statement_calculator.rb b/app/services/finance/ecf/mentor/statement_calculator.rb new file mode 100644 index 0000000000..1d373056e8 --- /dev/null +++ b/app/services/finance/ecf/mentor/statement_calculator.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Finance + module ECF + module Mentor + class StatementCalculator < Finance::ECF::StatementCalculator + def self.event_types + %i[ + started + completed + ] + end + + def self.event_types_for_display + %i[ + started + completed + ] + end + + def voided_declarations + statement.participant_declarations.voided.where(type: "ParticipantDeclaration::Mentor") + end + + event_types.each do |event_type| + define_method "#{event_type}_fee_for_declaration" do + fee_for_declaration(type: event_type) + end + + define_method "additions_for_#{event_type}" do + hash = {}.merge(*output_calculator.output_breakdown) + hash[:"#{event_type}_additions"] * output_calculator.fee_for_declaration(type: event_type) + end + + define_method "deductions_for_#{event_type}" do + hash = {}.merge(*output_calculator.output_breakdown) + hash[:"#{event_type}_subtractions"] * output_calculator.fee_for_declaration(type: event_type) + end + end + + def fee_for_declaration(type:) + output_calculator.fee_for_declaration(type:) + end + + def started_count + hash = {}.merge(*output_calculator.output_breakdown) + hash[:started_additions] + end + + def completed_count + hash = {}.merge(*output_calculator.output_breakdown) + hash[:completed_additions] + end + + def adjustments_total + -clawback_deductions + end + + def clawback_deductions + event_types.sum do |event_type| + public_send(:"deductions_for_#{event_type}") + end + end + + def total(with_vat: false) + sum = output_fee + adjustments_total + sum += vat if with_vat + sum + end + + def event_types_for_display + self.class.event_types_for_display + end + + private + + def output_calculator + @output_calculator ||= OutputCalculator.new(statement:) + end + end + end + end +end diff --git a/app/services/finance/ecf/output_calculator.rb b/app/services/finance/ecf/output_calculator.rb index 21dcae98f2..42c566f6f6 100644 --- a/app/services/finance/ecf/output_calculator.rb +++ b/app/services/finance/ecf/output_calculator.rb @@ -3,249 +3,16 @@ module Finance module ECF class OutputCalculator + COHORT_WITH_NO_MENTOR_FUNDING_DECLARATION_CLASS_TYPES = ["ParticipantDeclaration::ECT", "ParticipantDeclaration::Mentor"].freeze + attr_reader :statement def initialize(statement:) @statement = statement end - def banding_breakdown - @banding_breakdown ||= begin - bandings = declaration_types.map do |declaration_type| - current_banding_for_declaration_type(declaration_type) - end - - result = bandings[0] - - band_letters.each do |letter| - bandings[1..].each do |banding| - new_chunk = banding.find { |e| e[:band] == letter } - result.find { |e| e[:band] == letter }.merge!(new_chunk) - end - end - - result - end - end - - def uplift_breakdown - @uplift_breakdown ||= { - previous_count: previous_fill_level_for_uplift, - count: current_billable_count_for_uplift - current_refundable_count_for_uplift, - additions: current_billable_count_for_uplift, - subtractions: current_refundable_count_for_uplift, - } - end - - def fee_for_declaration(band_letter:, type:) - percentage = case type - when :started - started_event_percentage - when :completed - completed_event_percentage - when :retained_1, :retained_2, :retained_3, :retained_4 - retained_event_percentage - when :extended_1, :extended_2, :extended_3 - extended_event_percentage - end - - percentage * band_for_letter(band_letter).output_payment_per_participant - end - - private - - def band_for_letter(letter) - bands.zip(:a..:z).find { |e| e[1] == letter }[0] - end - - def started_event_percentage - 0.2 - end - - def completed_event_percentage - 0.2 - end - - def retained_event_percentage - 0.15 - end - - def extended_event_percentage - 0.15 - end - - def declaration_types - %w[ - started - retained-1 - retained-2 - retained-3 - retained-4 - completed - extended-1 - extended-2 - extended-3 - ] - end - - # this is a 3 pass algorithm - # first pass adds billable declarations - # second pass subtracts refunds - # third pass further subtracts refunds if statement is net negative - def current_banding_for_declaration_type(declaration_type) - pot_size = current_billable_count_for_declaration_type(declaration_type) - - banding = previous_banding_for_declaration_type(declaration_type).map do |hash| - band_capacity = hash[:max] - (hash[:min] - 1) - hash[:"previous_#{declaration_type.underscore}_count"] - - fill_level = [pot_size, band_capacity].min - - pot_size -= fill_level - - hash[:"#{declaration_type.underscore}_count"] = fill_level - hash[:"#{declaration_type.underscore}_additions"] = fill_level - - hash - end - - pot_size = current_refundable_count_declaration_type(declaration_type) - - banding = banding.reverse.map do |hash| - fill_level = hash[:"#{declaration_type.underscore}_count"] - - available = [fill_level, pot_size].min - - hash[:"#{declaration_type.underscore}_count"] = fill_level - available - hash[:"#{declaration_type.underscore}_subtractions"] = available - - pot_size -= available - - hash - end - - if pot_size.positive? - banding = banding.map do |hash| - available = hash[:"previous_#{declaration_type.underscore}_count"] - - unless available.zero? - reduction = [pot_size, available].min - - hash[:"#{declaration_type.underscore}_count"] = -reduction - hash[:"#{declaration_type.underscore}_subtractions"] += reduction - - pot_size -= reduction - end - - hash - end - end - - banding.reverse - end - - def previous_banding_for_declaration_type(declaration_type) - pot_size = previous_fill_level_for_declaration_type(declaration_type) - - bands.zip(:a..:z).map do |band, letter| - # minimum band should always be 1 or more, otherwise band a will go over - # its max limit - band_min = band.min.to_i.zero? ? 1 : band.min - - band_capacity = band.max - band_min + 1 - - fill_level = [pot_size, band_capacity].min - - pot_size -= fill_level - - key_name = "previous_#{declaration_type.underscore}_count".to_sym - - { - band: letter, - min: band_min, - max: band.max, - key_name => fill_level, - } - end - end - - def bands - statement.contract.bands.order(max: :asc) - end - - def band_letters - bands.zip(:a..:z).map { |e| e[1] } - end - - def previous_fill_level_for_declaration_type(declaration_type) - billable = Finance::StatementLineItem - .where(statement: statement.previous_statements) - .billable - .joins(:participant_declaration) - .where(participant_declarations: { declaration_type: }) - .count - - refundable = Finance::StatementLineItem - .where(statement: statement.previous_statements) - .refundable - .joins(:participant_declaration) - .where(participant_declarations: { declaration_type: }) - .count - - billable - refundable - end - - def previous_fill_level_for_uplift - billable = Finance::StatementLineItem - .where(statement: statement.previous_statements) - .billable - .joins(:participant_declaration) - .where(participant_declarations: { declaration_type: "started" }) - .where("participant_declarations.sparsity_uplift = true OR participant_declarations.pupil_premium_uplift = true") - .count - - refundable = Finance::StatementLineItem - .where(statement: statement.previous_statements) - .refundable - .joins(:participant_declaration) - .where(participant_declarations: { declaration_type: "started" }) - .where("participant_declarations.sparsity_uplift = true OR participant_declarations.pupil_premium_uplift = true") - .count - - billable - refundable - end - - def current_billable_count_for_declaration_type(declaration_type) - statement - .billable_statement_line_items - .joins(:participant_declaration) - .where(participant_declarations: { declaration_type: }) - .count - end - - def current_refundable_count_declaration_type(declaration_type) - statement - .refundable_statement_line_items - .joins(:participant_declaration) - .where(participant_declarations: { declaration_type: }) - .count - end - - def current_billable_count_for_uplift - statement - .billable_statement_line_items - .joins(:participant_declaration) - .where(participant_declarations: { declaration_type: "started" }) - .where("participant_declarations.sparsity_uplift = true OR participant_declarations.pupil_premium_uplift = true") - .count - end - - def current_refundable_count_for_uplift - statement - .refundable_statement_line_items - .joins(:participant_declaration) - .where(participant_declarations: { declaration_type: "started" }) - .where("participant_declarations.sparsity_uplift = true OR participant_declarations.pupil_premium_uplift = true") - .count + def participant_declaration_class_types + @participant_declaration_class_types ||= statement.cohort.mentor_funding? ? self.class::COHORT_WITH_MENTOR_FUNDING_DECLARATION_CLASS_TYPES : COHORT_WITH_NO_MENTOR_FUNDING_DECLARATION_CLASS_TYPES end end end diff --git a/app/services/finance/ecf/statement_calculator.rb b/app/services/finance/ecf/statement_calculator.rb index ce982d3341..d26aad8b3e 100644 --- a/app/services/finance/ecf/statement_calculator.rb +++ b/app/services/finance/ecf/statement_calculator.rb @@ -1,44 +1,8 @@ # frozen_string_literal: true -require "payment_calculator/ecf/service_fees" - module Finance module ECF class StatementCalculator - def self.event_types - %i[ - started - retained_1 - retained_2 - retained_3 - retained_4 - completed - extended_1 - extended_2 - extended_3 - ] - end - - def self.event_types_for_display - %i[ - started - retained_1 - retained_2 - retained_3 - retained_4 - completed - ] - end - - def self.band_mapping - { - a: 0, - b: 1, - c: 2, - d: 3, - } - end - attr_reader :statement delegate :contract, to: :statement @@ -47,143 +11,18 @@ def initialize(statement:) @statement = statement end - def bands - @bands ||= statement.contract.bands.order(max: :asc) - end - - def band_letters - (:a..:z).take(bands.size) - end - def vat total * vat_rate end def voided_declarations - statement.participant_declarations.voided - end - - event_types.each do |event_type| - band_mapping.each_key do |letter| - define_method "#{event_type}_band_#{letter}_count" do - output_calculator.banding_breakdown.find { |e| e[:band] == letter }[:"#{event_type}_count"] - end - - define_method "#{event_type}_band_#{letter}_additions" do - output_calculator.banding_breakdown.find { |e| e[:band] == letter }[:"#{event_type}_additions"] - end - - define_method "#{event_type}_band_#{letter}_fee_per_declaration" do - output_calculator.fee_for_declaration(band_letter: letter, type: event_type) - end - end - - define_method "additions_for_#{event_type}" do - output_calculator.banding_breakdown.sum do |hash| - hash[:"#{event_type}_additions"] * output_calculator.fee_for_declaration(band_letter: hash[:band], type: event_type) - end - end - - define_method "deductions_for_#{event_type}" do - output_calculator.banding_breakdown.sum do |hash| - hash[:"#{event_type}_subtractions"] * output_calculator.fee_for_declaration(band_letter: hash[:band], type: event_type) - end - end - end - - band_mapping.each_key do |letter| - define_method "extended_band_#{letter}_additions" do - send("extended_1_band_#{letter}_additions") + - send("extended_2_band_#{letter}_additions") + - send("extended_3_band_#{letter}_additions") - end - - define_method "extended_band_#{letter}_fee_per_declaration" do - send("extended_1_band_#{letter}_fee_per_declaration") - end - end - - def additions_for_extended - additions_for_extended_1 + additions_for_extended_2 + additions_for_extended_3 - end - - def fee_for_declaration(band_letter:, type:) - output_calculator.fee_for_declaration(band_letter:, type:) - end - - def started_count - output_calculator.banding_breakdown.sum do |hash| - hash[:started_additions] - end - end - - def retained_count - output_calculator.banding_breakdown.sum do |hash| - hash.select { |k, _| k.match(/retained_\d_additions/) }.values.sum - end - end - - def completed_count - output_calculator.banding_breakdown.sum do |hash| - hash[:completed_additions] - end - end - - def extended_count - output_calculator.banding_breakdown.sum do |hash| - hash.select { |k, _| k.match(/extended_\d_additions/) }.values.sum - end + statement.participant_declarations.voided.where(type: output_calculator.participant_declaration_class_types) end def voided_count voided_declarations.count end - def uplift_count - output_calculator.uplift_breakdown[:count] - end - - def uplift_additions_count - return 0.0 unless statement.contract.include_uplift_fees? - - output_calculator.uplift_breakdown[:additions] - end - - def uplift_deductions_count - return 0 unless statement.contract.include_uplift_fees? - - output_calculator.uplift_breakdown[:subtractions] - end - - def uplift_fee_per_declaration - return 0.0 unless statement.contract.include_uplift_fees? - - statement.contract.uplift_amount - end - - def total_for_uplift - return 0.0 unless statement.contract.include_uplift_fees? - - previous_uplift_count = output_calculator.uplift_breakdown[:previous_count] - previous_uplift_amount = previous_uplift_count * uplift_fee_per_declaration - - # uplift_clawback_deductions is a negative number so doing a double negative -- - # we're adding it back as we had subtracted from adjustments_total - delta_uplift_amount = uplift_count * uplift_fee_per_declaration - uplift_clawback_deductions - - available = [(statement.contract.uplift_cap - previous_uplift_amount), 0].max - - [available, delta_uplift_amount].min - end - - def uplift_clawback_deductions - uplift_deductions_count * -uplift_fee_per_declaration - end - - def adjustments_total - -clawback_deductions + uplift_clawback_deductions - end - def additional_adjustments_total statement.adjustments.sum(:amount) end @@ -194,46 +33,18 @@ def clawback_deductions end end - def total(with_vat: false) - sum = service_fee + output_fee + total_for_uplift + adjustments_total + additional_adjustments_total + statement.reconcile_amount - sum += vat if with_vat - sum - end - - def service_fee - contract.monthly_service_fee || calculated_service_fee - end - def output_fee event_types.sum do |event_type| public_send(:"additions_for_#{event_type}") end end - def event_types_for_display - self.class.event_types_for_display.tap do |types| - types << :extended if extended_count.positive? - end - end - private - def calculated_service_fee - PaymentCalculator::ECF::ServiceFees.new(contract:).call.sum { |hash| hash[:monthly] } - end - - def output_calculator - @output_calculator ||= OutputCalculator.new(statement:) - end - def event_types self.class.event_types end - def band_mapping - self.class.band_mapping - end - def vat_rate lead_provider.vat_chargeable? ? 0.2 : 0 end diff --git a/app/views/finance/ecf/statements/show.html.erb b/app/views/finance/ecf/statements/show.html.erb index 85eaa7703e..5b8fc611bd 100644 --- a/app/views/finance/ecf/statements/show.html.erb +++ b/app/views/finance/ecf/statements/show.html.erb @@ -142,6 +142,143 @@ +<% if @statement.cohort.mentor_funding? %> + +<% end %>