Files
openfoodnetwork/app/services/orders/handle_fees_service.rb
Gaetan Craig-Riou 1279ab21a6 Fix delete fee logic
A fee can be associated to both the incoming and outgoing exchange, the
previous logic did not account for that, resulting in the fee not being
correctly removed.
Now the delete logic also check for the metadata enterprise role to see
if any additional fee need to be removed.
2025-04-01 13:46:34 +11:00

172 lines
5.6 KiB
Ruby

# frozen_string_literal: true
module Orders
class HandleFeesService # rubocop:disable Metrics/ClassLength
attr_reader :order
delegate :distributor, :order_cycle, to: :order
FeeValue = Struct.new(:fee, :role, keyword_init: true)
def initialize(order)
@order = order
end
def recreate_all_fees!
# `with_lock` acquires an exclusive row lock on order so no other
# requests can update it until the transaction is commited.
# See https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/locking/pessimistic.rb#L69
# and https://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE
order.with_lock do
EnterpriseFee.clear_order_adjustments order
# To prevent issue with fee being removed when a product is not linked to the order cycle
# anymore, we now create or update line item fees.
# Previously fees were deleted and recreated, like we still do for order fees.
create_or_update_line_item_fees!
create_order_fees!
end
tax_enterprise_fees! unless order.before_payment_state?
order.update_order!
end
def create_or_update_line_item_fees!
order.line_items.includes(:variant).each do |line_item|
# No fee associated with the line item so we just create them
if line_item.enterprise_fee_adjustments.blank?
create_line_item_fees!(line_item)
next
end
create_or_update_line_item_fee!(line_item)
# delete any fees removed from the Order Cycle
delete_removed_fees!(line_item)
end
end
def create_order_fees!
return unless order_cycle
calculator.create_order_adjustments_for order
end
def tax_enterprise_fees!
Spree::TaxRate.adjust(order, order.all_adjustments.enterprise_fee)
end
def update_line_item_fees!(line_item)
line_item.adjustments.enterprise_fee.each do |fee|
fee.update_adjustment!(line_item, force: true)
end
end
def update_order_fees!
order.adjustments.enterprise_fee.where(adjustable_type: 'Spree::Order').each do |fee|
fee.update_adjustment!(order, force: true)
end
end
private
def create_line_item_fees!(line_item)
return unless provided_by_order_cycle? line_item
calculator.create_line_item_adjustments_for(line_item)
end
def create_or_update_line_item_fee!(line_item)
applicators = calculator.per_item_enterprise_fee_applicators_for(line_item.variant)
applicators.each do |fee_applicator|
fee_adjustment = line_item.adjustments.by_originator_and_enterprise_role(
fee_applicator.enterprise_fee, fee_applicator.role
)
if fee_adjustment
fee_adjustment.update_adjustment!(line_item, force: true)
elsif provided_by_order_cycle? line_item
fee_applicator.create_line_item_adjustment(line_item)
end
end
# Update any fees not already processed
fees_to_update = order_cycle_fees.map(&:fee) - applicators.map(&:enterprise_fee)
update_fee_adjustments!(line_item, fees_to_update)
end
def update_fee_adjustments!(line_item, fees_to_update)
fees_to_update.each do |fee|
fee_adjustment = line_item.adjustments.find_by(originator: fee)
fee_adjustment&.update_adjustment!(line_item, force: true)
end
end
def delete_removed_fees!(line_item)
removed_fees = line_item.enterprise_fee_adjustments.where.not(
originator: order_cycle_fees.map(&:fee)
)
# The same fee can be used in the incoming and outgoing exchange, (supplier and distributor
# fees), so we need an extra check to see if a fee linked to both exchanges has been removed
order_cycle_fees.each do |order_cycle_fee|
# Check if there is any fee adjustment with a role other than the one in the order cycle fee
fee = line_item.enterprise_fee_adjustments.by_originator_and_not_enterprise_role(
order_cycle_fee.fee, order_cycle_fee.role
)
# Check if the fee matches a fee linked to the order cycle
if fee.nil? || order_cycle_fees_include_fee?(fee)
next
end
# If not linked to the order cycle we add it to the list of fee to be removed
removed_fees = removed_fees.to_a.push(fee)
end
removed_fees.each(&:destroy)
end
def order_cycle_fees
return @order_cycle_fees if defined? @order_cycle_fees
@order_cycle_fees = begin
fees = []
return fees unless order_cycle && distributor
order_cycle.exchanges.supplying_to(distributor).each do |exchange|
exchange.enterprise_fees.per_item.each do |enterprise_fee|
fee_value = FeeValue.new(fee: enterprise_fee, role: exchange.role)
fees << fee_value
end
end
order_cycle.coordinator_fees.per_item.each do |enterprise_fee|
fees << FeeValue.new(fee: enterprise_fee, role: "coordinator")
end
fees
end
end
def order_cycle_fees_include_fee?(fee)
matching = order_cycle_fees.select do |order_cycle_fee|
order_cycle_fee.fee == fee.originator &&
order_cycle_fee.role == fee.metadata.enterprise_role
end
matching.present?
end
def calculator
@calculator ||= OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle)
end
def provided_by_order_cycle?(line_item)
@order_cycle_variant_ids ||= order_cycle&.variants&.map(&:id) || []
@order_cycle_variant_ids.include? line_item.variant_id
end
end
end