mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-09 23:06:06 +00:00
See: https://github.com/openfoodfoundation/openfoodnetwork/blob/master/spec/system/admin/adjustments_spec.rb
132 lines
4.2 KiB
Ruby
132 lines
4.2 KiB
Ruby
# frozen_string_literal: false
|
|
|
|
module Spree
|
|
class DefaultTaxZoneValidator < ActiveModel::Validator
|
|
def validate(record)
|
|
return unless record.included_in_price
|
|
|
|
return if Zone.default_tax
|
|
|
|
record.errors.add(:included_in_price, Spree.t("errors.messages.included_price_validation"))
|
|
end
|
|
end
|
|
end
|
|
|
|
module Spree
|
|
class TaxRate < ApplicationRecord
|
|
acts_as_paranoid
|
|
include CalculatedAdjustments
|
|
|
|
belongs_to :zone, class_name: "Spree::Zone", inverse_of: :tax_rates, optional: true
|
|
belongs_to :tax_category, class_name: "Spree::TaxCategory", inverse_of: :tax_rates
|
|
has_many :adjustments, as: :originator, dependent: nil
|
|
|
|
validates :amount, presence: true, numericality: true
|
|
validates_with DefaultTaxZoneValidator
|
|
|
|
scope :by_zone, ->(zone) { where(zone_id: zone) }
|
|
|
|
# Gets the array of TaxRates appropriate for the specified order
|
|
def self.match(order)
|
|
return [] if order.distributor && !order.distributor.charges_sales_tax
|
|
return [] unless order.tax_zone
|
|
|
|
includes(zone: { zone_members: :zoneable }).load.select do |rate|
|
|
rate.potentially_applicable?(order.tax_zone)
|
|
end
|
|
end
|
|
|
|
def self.adjust(order, items)
|
|
applicable_rates = match(order)
|
|
applicable_tax_categories = applicable_rates.map(&:tax_category)
|
|
|
|
relevant_items, non_relevant_items = items.partition do |item|
|
|
applicable_tax_categories.include?(item.tax_category)
|
|
end
|
|
|
|
relevant_items.each do |item|
|
|
item.adjustments.tax.delete_all
|
|
relevant_rates = applicable_rates.select { |rate| rate.tax_category == item.tax_category }
|
|
relevant_rates.each do |rate|
|
|
rate.adjust(order, item)
|
|
end
|
|
end
|
|
|
|
non_relevant_items.each do |item|
|
|
if item.adjustments.tax.present?
|
|
item.adjustments.tax.delete_all
|
|
Spree::ItemAdjustments.new(item).update
|
|
end
|
|
end
|
|
end
|
|
|
|
# For VAT, the default rate is the rate that is configured for the default category
|
|
# It is needed for every price calculation (as all customer facing prices include VAT)
|
|
# Here we return the actual amount, which may be 0 in case of wrong setup, but is never nil
|
|
def self.default
|
|
category = TaxCategory.includes(:tax_rates).find_by(is_default: true)
|
|
return 0 unless category
|
|
|
|
address ||= Address.new(country_id: DefaultCountry.id)
|
|
rate = category.tax_rates
|
|
.detect { |tax_rate| tax_rate.zone.contains_address? address }.try(:amount)
|
|
|
|
rate || 0
|
|
end
|
|
|
|
def potentially_applicable?(order_tax_zone)
|
|
# If the rate's zone matches the order's tax zone, then it's applicable.
|
|
zone == order_tax_zone ||
|
|
# If the rate's zone *contains* the order's tax zone, then it's applicable.
|
|
zone.contains?(order_tax_zone) ||
|
|
# The rate's zone is the default zone, then it's always applicable.
|
|
(included_in_price? && zone.default_tax)
|
|
end
|
|
|
|
# Creates necessary tax adjustments for the item.
|
|
def adjust(order, item)
|
|
amount = compute_amount(item)
|
|
return if amount.zero?
|
|
|
|
included = included_in_price && default_zone_or_zone_match?(order)
|
|
|
|
adjustments.create!(
|
|
adjustable: item,
|
|
amount:,
|
|
order:,
|
|
label: create_label(amount),
|
|
included:
|
|
)
|
|
end
|
|
|
|
# This method is used by Adjustment#update to recalculate the cost.
|
|
def compute_amount(item)
|
|
if included_in_price
|
|
if default_zone_or_zone_match?(item.order)
|
|
calculator.compute(item)
|
|
else
|
|
# In this case, it's a refund (for instance offering a manual discount via an adjustment)
|
|
calculator.compute(item) * - 1
|
|
end
|
|
else
|
|
calculator.compute(item)
|
|
end
|
|
end
|
|
|
|
def default_zone_or_zone_match?(order)
|
|
Zone.default_tax&.contains?(order.tax_zone) || order.tax_zone == zone
|
|
end
|
|
|
|
private
|
|
|
|
def create_label(adjustment_amount)
|
|
label = ""
|
|
label << "#{Spree.t(:refund)} " if adjustment_amount.negative?
|
|
label << "#{name.presence || tax_category.name} "
|
|
label << (show_rate_in_label? ? "#{amount * 100}%" : "")
|
|
label << " (#{I18n.t('models.tax_rate.included_in_price')})" if included_in_price?
|
|
label
|
|
end
|
|
end
|
|
end
|