Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor amounts calculation #1188

Merged
merged 15 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/classes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def bulk_pfmp_completion
def update_bulk_pfmp
@pfmps = bulk_pfmp_completion_params[:pfmps].map do |pfmp_params|
@classe.pfmps.find(pfmp_params[:id]).tap do |pfmp|
pfmp.day_count = pfmp_params[:day_count]
PfmpManager.new(pfmp).update(day_count: pfmp_params[:day_count])
end
end

Expand Down
4 changes: 2 additions & 2 deletions app/controllers/pfmps_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def edit; end
def create
@pfmp = Pfmp.new(pfmp_params.merge(schooling: @schooling))

if @pfmp.save
if PfmpManager.new(@pfmp).update(pfmp_params)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

La fonction 'create' et 'update' n'avaient pas la même condition initialement, mais maintenant elles vérifient toutes les 2 'PfmpManager.new(@PFMP).update(pfmp_params)', est-ce normal ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oui normalement c'est équivalent parce que #update et #save retournent true quand les validations sont passées et false quand ca n'est pas le cas. Et ici on veut juste déclencher explicitement le recalcul.

redirect_to student_path(@schooling.student),
notice: t("pfmps.new.success")
else
Expand All @@ -31,7 +31,7 @@ def create
end

def update
if @pfmp.update(pfmp_params)
if PfmpManager.new(@pfmp).update(pfmp_params)
redirect_to school_year_class_schooling_pfmp_path(selected_school_year, @classe, @schooling, @pfmp),
notice: t("pfmps.edit.success")
else
Expand Down
41 changes: 0 additions & 41 deletions app/models/concerns/pfmp_amount_calculator.rb

This file was deleted.

36 changes: 13 additions & 23 deletions app/models/pfmp.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# frozen_string_literal: true

class Pfmp < ApplicationRecord # rubocop:disable Metrics/ClassLength
include PfmpAmountCalculator

TRANSITION_CLASS = PfmpTransition
STATE_MACHINE_CLASS = PfmpStateMachine
TRANSITION_RELATION_NAME = :transitions
Expand Down Expand Up @@ -50,6 +48,8 @@ class Pfmp < ApplicationRecord # rubocop:disable Metrics/ClassLength
comparison: { greater_than_or_equal_to: :start_date },
if: -> { start_date && end_date }

validates :amount, numericality: { only_integer: true, allow_nil: true, greater_than_or_equal_to: 0 }

validates :day_count,
numericality: {
only_integer: true,
Expand All @@ -66,25 +66,6 @@ class Pfmp < ApplicationRecord # rubocop:disable Metrics/ClassLength

before_destroy :ensure_destroyable?, prepend: true

after_save do
if day_count.present?
transition_to!(:completed) if in_state?(:pending)
elsif in_state?(:completed, :validated)
transition_to!(:pending)
end
end

after_save :recalculate_amounts_if_needed

# Recalculate amounts for the current PFMP and all follow up PFMPs that are still modifiable
def recalculate_amounts_if_needed
changed_day_count = day_count_before_last_save != day_count

return if !changed_day_count

PfmpManager.new(self).recalculate_amounts!
end

def validate!
transition_to!(:validated)
end
Expand Down Expand Up @@ -156,15 +137,24 @@ def can_retrigger_payment?
latest_payment_request.failed?
end

def all_pfmps_for_mef
student.pfmps
.joins(schooling: :classe)
.where("classes.mef_id": mef.id, "classes.school_year_id": school_year.id)
end

private

def amounts_yearly_cap
return unless mef

pfmps = all_pfmps_for_mef
cap = mef.wage.yearly_cap
total = all_pfmps_for_mef.sum(:amount)
total = pfmps.sum(:amount)
return unless total > cap

errors.add(:amount, "Yearly cap of #{cap} not respected for Mef code: #{mef.code}")
errors.add(:amount,
"Yearly cap of #{cap} not respected for Mef code: #{mef.code} \\
-> #{total}/#{cap} with #{pfmps.count} PFMPs")
end
end
70 changes: 62 additions & 8 deletions app/services/pfmp_manager.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

# Simple Ruby service to Handle complex actions on PFMPs
# TODO: refactor PfmpAmountCalculator to be part of this class (requires a lot of spec changes)

class PfmpManager
class PfmpManagerError < StandardError; end
Expand All @@ -15,12 +14,29 @@ def initialize(pfmp)
@pfmp = pfmp
end

def update(params)
update!(params)
true
rescue ActiveRecord::RecordInvalid
false
end

def update!(params)
old_day_count = pfmp.day_count
pfmp.update!(params)

recalculate_amounts! if params[:day_count].present? && old_day_count != params[:day_count]

transition!
end

def recalculate_amounts!
raise PfmpNotModifiableError unless pfmp.can_be_modified?

ApplicationRecord.transaction do
pfmp.update!(amount: pfmp.calculate_amount)
rebalance_following_pfmps!
Pfmp.transaction do
pfmp.all_pfmps_for_mef.lock!
pfmp.update!(amount: calculate_amount)
rebalance_other_pfmps!
end
end

Expand All @@ -41,13 +57,29 @@ def retry_incomplete_payment_request!
end

def rectify_and_update_attributes!(confirmed_pfmp_params, confirmed_address_params)
ApplicationRecord.transaction do
Pfmp.transaction do
@pfmp.update!(confirmed_pfmp_params)
@pfmp.student.update!(confirmed_address_params)
@pfmp.rectify!
end
end

def calculate_amount
return 0 if pfmp.day_count.nil?

[
pfmp.day_count * pfmp.wage.daily_rate,
pfmp.wage.yearly_cap - previously_locked_amount
].min
end

def previously_locked_amount
other_priced_pfmps
.map(&:amount)
.compact
.sum
end

def retry_payment_request!(reasons)
return unless @pfmp.latest_payment_request&.eligible_for_rejected_or_unpaid_auto_retry?(reasons)

Expand All @@ -57,9 +89,31 @@ def retry_payment_request!(reasons)

private

def rebalance_following_pfmps!
pfmp.rebalancable_pfmps.each do |pfmp|
pfmp.update!(amount: pfmp.calculate_amount)
def other_pfmps_for_mef
pfmp.all_pfmps_for_mef.excluding(pfmp)
end

def other_priced_pfmps
other_pfmps_for_mef
.where.not(amount: nil)
end

def rebalancable_pfmps
@rebalancable_pfmps ||= other_pfmps_for_mef
.select(&:can_be_rebalanced?)
end

def rebalance_other_pfmps!
rebalancable_pfmps.each do |rebalancable_pfmp|
rebalancable_pfmp.update!(amount: PfmpManager.new(rebalancable_pfmp).calculate_amount)
end
end

def transition!
if pfmp.day_count.present?
pfmp.transition_to!(:completed) if pfmp.in_state?(:pending)
elsif pfmp.in_state?(:completed, :validated)
pfmp.transition_to!(:pending)
end
end
end
2 changes: 1 addition & 1 deletion app/views/pfmps/_payment_panel.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
= number_to_currency pfmp.wage.yearly_cap
de plafond maximum par an
%div.fr-mb-1w
= number_to_currency pfmp.previously_locked_amount
= number_to_currency PfmpManager.new(pfmp).previously_locked_amount
déjà engagés par d'autres PFMPs sur cette scolarité.
11 changes: 10 additions & 1 deletion spec/factories/pfmps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
end

trait :completed do
day_count { rand(1..6) } # lovely roll dice
transient do
day_count { 3 }
end
after(:create) do |pfmp, eval|
PfmpManager.new(pfmp).update!(day_count: eval.day_count)
end
end

trait :can_be_validated do
Expand Down Expand Up @@ -44,5 +49,9 @@
pfmp.reload.rectify!
end
end

after(:build) do |pfmp, _|
PfmpManager.new(pfmp).update!(day_count: pfmp.day_count)
end
end
end
Loading