mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-28 21:07:16 +00:00
Merge pull request #5883 from luisramos0/taxs
[Bye bye Spree] Bring models tax_rate, tax_categories, adjustments and calculator from spree_core
This commit is contained in:
177
app/models/spree/adjustment.rb
Normal file
177
app/models/spree/adjustment.rb
Normal file
@@ -0,0 +1,177 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spree/localized_number'
|
||||
require 'concerns/adjustment_scopes'
|
||||
|
||||
# Adjustments represent a change to the +item_total+ of an Order. Each adjustment
|
||||
# has an +amount+ that can be either positive or negative.
|
||||
#
|
||||
# Adjustments can be open/closed/finalized
|
||||
#
|
||||
# Once an adjustment is finalized, it cannot be changed, but an adjustment can
|
||||
# toggle between open/closed as needed
|
||||
#
|
||||
# Boolean attributes:
|
||||
#
|
||||
# +mandatory+
|
||||
#
|
||||
# If this flag is set to true then it means the the charge is required and will not
|
||||
# be removed from the order, even if the amount is zero. In other words a record
|
||||
# will be created even if the amount is zero. This is useful for representing things
|
||||
# such as shipping and tax charges where you may want to make it explicitly clear
|
||||
# that no charge was made for such things.
|
||||
#
|
||||
# +eligible?+
|
||||
#
|
||||
# This boolean attributes stores whether this adjustment is currently eligible
|
||||
# for its order. Only eligible adjustments count towards the order's adjustment
|
||||
# total. This allows an adjustment to be preserved if it becomes ineligible so
|
||||
# it might be reinstated.
|
||||
module Spree
|
||||
class Adjustment < ActiveRecord::Base
|
||||
extend Spree::LocalizedNumber
|
||||
|
||||
# Deletion of metadata is handled in the database.
|
||||
# So we don't need the option `dependent: :destroy` as long as
|
||||
# AdjustmentMetadata has no destroy logic itself.
|
||||
has_one :metadata, class_name: 'AdjustmentMetadata'
|
||||
|
||||
belongs_to :adjustable, polymorphic: true
|
||||
belongs_to :source, polymorphic: true
|
||||
belongs_to :originator, polymorphic: true
|
||||
belongs_to :tax_rate, -> { where spree_adjustments: { originator_type: 'Spree::TaxRate' } },
|
||||
foreign_key: 'originator_id'
|
||||
|
||||
validates :label, presence: true
|
||||
validates :amount, numericality: true
|
||||
|
||||
after_save :update_adjustable
|
||||
after_destroy :update_adjustable
|
||||
|
||||
state_machine :state, initial: :open do
|
||||
event :close do
|
||||
transition from: :open, to: :closed
|
||||
end
|
||||
|
||||
event :open do
|
||||
transition from: :closed, to: :open
|
||||
end
|
||||
|
||||
event :finalize do
|
||||
transition from: [:open, :closed], to: :finalized
|
||||
end
|
||||
end
|
||||
|
||||
scope :tax, -> { where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::Order') }
|
||||
scope :price, -> { where(adjustable_type: 'Spree::LineItem') }
|
||||
scope :optional, -> { where(mandatory: false) }
|
||||
scope :charge, -> { where('amount >= 0') }
|
||||
scope :credit, -> { where('amount < 0') }
|
||||
scope :return_authorization, -> { where(source_type: "Spree::ReturnAuthorization") }
|
||||
|
||||
scope :enterprise_fee, -> { where(originator_type: 'EnterpriseFee') }
|
||||
scope :admin, -> { where(source_type: nil, originator_type: nil) }
|
||||
scope :included_tax, -> {
|
||||
where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem')
|
||||
}
|
||||
|
||||
scope :with_tax, -> { where('spree_adjustments.included_tax <> 0') }
|
||||
scope :without_tax, -> { where('spree_adjustments.included_tax = 0') }
|
||||
scope :payment_fee, -> { where(AdjustmentScopes::PAYMENT_FEE_SCOPE) }
|
||||
scope :shipping, -> { where(AdjustmentScopes::SHIPPING_SCOPE) }
|
||||
scope :eligible, -> { where(AdjustmentScopes::ELIGIBLE_SCOPE) }
|
||||
|
||||
localize_number :amount
|
||||
|
||||
# Update the boolean _eligible_ attribute which determines which adjustments
|
||||
# count towards the order's adjustment_total.
|
||||
def set_eligibility
|
||||
result = mandatory || (amount != 0 && eligible_for_originator?)
|
||||
update_column(:eligible, result)
|
||||
end
|
||||
|
||||
# Allow originator of the adjustment to perform an additional eligibility of the adjustment
|
||||
# Should return _true_ if originator is absent or doesn't implement _eligible?_
|
||||
def eligible_for_originator?
|
||||
return true if originator.nil?
|
||||
|
||||
!originator.respond_to?(:eligible?) || originator.eligible?(source)
|
||||
end
|
||||
|
||||
# Update both the eligibility and amount of the adjustment. Adjustments
|
||||
# delegate updating of amount to their Originator when present, but only if
|
||||
# +locked+ is false. Adjustments that are +locked+ will never change their amount.
|
||||
#
|
||||
# Adjustments delegate updating of amount to their Originator when present,
|
||||
# but only if when they're in "open" state, closed or finalized adjustments
|
||||
# are not recalculated.
|
||||
#
|
||||
# It receives +calculable+ as the updated source here so calculations can be
|
||||
# performed on the current values of that source. If we used +source+ it
|
||||
# could load the old record from db for the association. e.g. when updating
|
||||
# more than on line items at once via accepted_nested_attributes the order
|
||||
# object on the association would be in a old state and therefore the
|
||||
# adjustment calculations would not performed on proper values
|
||||
def update!(calculable = nil)
|
||||
return if immutable?
|
||||
|
||||
# Fix for Spree issue #3381
|
||||
# If we attempt to call 'source' before the reload, then source is currently
|
||||
# the order object. After calling a reload, the source is the Shipment.
|
||||
reload
|
||||
originator.update_adjustment(self, calculable || source) if originator.present?
|
||||
set_eligibility
|
||||
end
|
||||
|
||||
def currency
|
||||
adjustable ? adjustable.currency : Spree::Config[:currency]
|
||||
end
|
||||
|
||||
def display_amount
|
||||
Spree::Money.new(amount, currency: currency)
|
||||
end
|
||||
|
||||
def immutable?
|
||||
state != "open"
|
||||
end
|
||||
|
||||
def set_included_tax!(rate)
|
||||
tax = amount - (amount / (1 + rate))
|
||||
set_absolute_included_tax! tax
|
||||
end
|
||||
|
||||
def set_absolute_included_tax!(tax)
|
||||
# This rubocop issue can now fixed by renaming Adjustment#update! to something else,
|
||||
# then AR's update! can be used instead of update_attributes!
|
||||
# rubocop:disable Rails/ActiveRecordAliases
|
||||
update_attributes! included_tax: tax.round(2)
|
||||
# rubocop:enable Rails/ActiveRecordAliases
|
||||
end
|
||||
|
||||
def display_included_tax
|
||||
Spree::Money.new(included_tax, currency: currency)
|
||||
end
|
||||
|
||||
def has_tax?
|
||||
included_tax.positive?
|
||||
end
|
||||
|
||||
def self.without_callbacks
|
||||
skip_callback :save, :after, :update_adjustable
|
||||
skip_callback :destroy, :after, :update_adjustable
|
||||
|
||||
result = yield
|
||||
ensure
|
||||
set_callback :save, :after, :update_adjustable
|
||||
set_callback :destroy, :after, :update_adjustable
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_adjustable
|
||||
adjustable.update! if adjustable.is_a? Order
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
require 'spree/localized_number'
|
||||
require 'concerns/adjustment_scopes'
|
||||
|
||||
module Spree
|
||||
Adjustment.class_eval do
|
||||
extend Spree::LocalizedNumber
|
||||
|
||||
# Deletion of metadata is handled in the database.
|
||||
# So we don't need the option `dependent: :destroy` as long as
|
||||
# AdjustmentMetadata has no destroy logic itself.
|
||||
has_one :metadata, class_name: 'AdjustmentMetadata'
|
||||
belongs_to :tax_rate, -> { where spree_adjustments: { originator_type: 'Spree::TaxRate' } },
|
||||
foreign_key: 'originator_id'
|
||||
|
||||
scope :enterprise_fee, -> { where(originator_type: 'EnterpriseFee') }
|
||||
scope :admin, -> { where(source_type: nil, originator_type: nil) }
|
||||
scope :included_tax, -> {
|
||||
where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem')
|
||||
}
|
||||
|
||||
scope :with_tax, -> { where('spree_adjustments.included_tax <> 0') }
|
||||
scope :without_tax, -> { where('spree_adjustments.included_tax = 0') }
|
||||
scope :payment_fee, -> { where(AdjustmentScopes::PAYMENT_FEE_SCOPE) }
|
||||
scope :shipping, -> { where(AdjustmentScopes::SHIPPING_SCOPE) }
|
||||
scope :eligible, -> { where(AdjustmentScopes::ELIGIBLE_SCOPE) }
|
||||
|
||||
localize_number :amount
|
||||
|
||||
def set_included_tax!(rate)
|
||||
tax = amount - (amount / (1 + rate))
|
||||
set_absolute_included_tax! tax
|
||||
end
|
||||
|
||||
def set_absolute_included_tax!(tax)
|
||||
# This rubocop issue can only be fixed when Adjustment#update! is brought from Spree to OFN
|
||||
# and renamed to something else, then AR's update! can be used instead of update_attributes!
|
||||
# rubocop:disable Rails/ActiveRecordAliases
|
||||
update_attributes! included_tax: tax.round(2)
|
||||
# rubocop:enable Rails/ActiveRecordAliases
|
||||
end
|
||||
|
||||
def display_included_tax
|
||||
Spree::Money.new(included_tax, currency: currency)
|
||||
end
|
||||
|
||||
def has_tax?
|
||||
included_tax > 0
|
||||
end
|
||||
|
||||
def self.without_callbacks
|
||||
skip_callback :save, :after, :update_adjustable
|
||||
skip_callback :destroy, :after, :update_adjustable
|
||||
|
||||
result = yield
|
||||
ensure
|
||||
set_callback :save, :after, :update_adjustable
|
||||
set_callback :destroy, :after, :update_adjustable
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
56
app/models/spree/calculator.rb
Normal file
56
app/models/spree/calculator.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class Calculator < ActiveRecord::Base
|
||||
belongs_to :calculable, polymorphic: true
|
||||
|
||||
# This method must be overriden in concrete calculator.
|
||||
#
|
||||
# It should return amount computed based on #calculable and/or optional parameter
|
||||
def compute(_something = nil)
|
||||
raise NotImplementedError, 'please use concrete calculator'
|
||||
end
|
||||
|
||||
# overwrite to provide description for your calculators
|
||||
def self.description
|
||||
'Base Calculator'
|
||||
end
|
||||
|
||||
###################################################################
|
||||
|
||||
def self.register(*klasses); end
|
||||
|
||||
# Returns all calculators applicable for kind of work
|
||||
def self.calculators
|
||||
Rails.application.config.spree.calculators
|
||||
end
|
||||
|
||||
def to_s
|
||||
self.class.name.titleize.gsub("Calculator\/", "")
|
||||
end
|
||||
|
||||
def description
|
||||
self.class.description
|
||||
end
|
||||
|
||||
def available?(_object)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Given an object which might be an Order or a LineItem (amongst
|
||||
# others), return a collection of line items.
|
||||
def line_items_for(object)
|
||||
if object.is_a?(Spree::LineItem)
|
||||
[object]
|
||||
elsif object.respond_to? :line_items
|
||||
object.line_items
|
||||
elsif object.respond_to?(:order) && object.order.present?
|
||||
object.order.line_items
|
||||
else
|
||||
[object]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
module Spree
|
||||
Calculator.class_eval do
|
||||
private
|
||||
|
||||
# Given an object which might be an Order or a LineItem (amongst
|
||||
# others), return a collection of line items.
|
||||
def line_items_for(object)
|
||||
if object.is_a?(Spree::LineItem)
|
||||
[object]
|
||||
elsif object.respond_to? :line_items
|
||||
object.line_items
|
||||
elsif object.respond_to?(:order) && object.order.present?
|
||||
object.order.line_items
|
||||
else
|
||||
[object]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -179,11 +179,11 @@ module Spree
|
||||
# For more information, please see Spree::Payment#set_unique_identifier
|
||||
order_id: gateway_order_id }
|
||||
|
||||
options.merge!({ shipping: order.ship_total * 100,
|
||||
tax: order.tax_total * 100,
|
||||
subtotal: order.item_total * 100,
|
||||
discount: order.promo_total * 100,
|
||||
currency: currency })
|
||||
options.merge!(shipping: order.ship_total * 100,
|
||||
tax: order.tax_total * 100,
|
||||
subtotal: order.item_total * 100,
|
||||
discount: 0,
|
||||
currency: currency)
|
||||
|
||||
options.merge!({ billing_address: order.bill_address.try(:active_merchant_hash),
|
||||
shipping_address: order.ship_address.try(:active_merchant_hash) })
|
||||
|
||||
20
app/models/spree/tax_category.rb
Normal file
20
app/models/spree/tax_category.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class TaxCategory < ActiveRecord::Base
|
||||
acts_as_paranoid
|
||||
validates :name, presence: true, uniqueness: { scope: :deleted_at }
|
||||
|
||||
has_many :tax_rates, dependent: :destroy
|
||||
|
||||
before_save :set_default_category
|
||||
|
||||
def set_default_category
|
||||
# set existing default tax category to false if this one has been marked as default
|
||||
|
||||
return unless is_default && tax_category = self.class.find_by(is_default: true)
|
||||
|
||||
tax_category.update_column(:is_default, false) unless tax_category == self
|
||||
end
|
||||
end
|
||||
end
|
||||
132
app/models/spree/tax_rate.rb
Normal file
132
app/models/spree/tax_rate.rb
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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(:included_price_validation))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Spree
|
||||
class TaxRate < ActiveRecord::Base
|
||||
acts_as_paranoid
|
||||
include Spree::Core::CalculatedAdjustments
|
||||
belongs_to :zone, class_name: "Spree::Zone"
|
||||
belongs_to :tax_category, class_name: "Spree::TaxCategory"
|
||||
|
||||
validates :amount, presence: true, numericality: true
|
||||
validates :tax_category_id, presence: 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
|
||||
|
||||
all.select do |rate|
|
||||
rate.zone == order.tax_zone || rate.zone.contains?(order.tax_zone) || rate.zone.default_tax
|
||||
end
|
||||
end
|
||||
|
||||
def self.adjust(order)
|
||||
order.adjustments.tax.destroy_all
|
||||
order.line_item_adjustments.where(originator_type: 'Spree::TaxRate').destroy_all
|
||||
|
||||
match(order).each do |rate|
|
||||
rate.adjust(order)
|
||||
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: Spree::Config[:default_country_id])
|
||||
rate = category.tax_rates.detect { |tax_rate| tax_rate.zone.include? address }.try(:amount)
|
||||
|
||||
rate || 0
|
||||
end
|
||||
|
||||
# Creates necessary tax adjustments for the order.
|
||||
def adjust(order)
|
||||
label = create_label
|
||||
if included_in_price
|
||||
if Zone.default_tax.contains? order.tax_zone
|
||||
order.line_items.each { |line_item| create_adjustment(label, line_item, line_item) }
|
||||
else
|
||||
amount = -1 * calculator.compute(order)
|
||||
label = Spree.t(:refund) + label
|
||||
|
||||
order.adjustments.create(
|
||||
amount: amount,
|
||||
source: order,
|
||||
originator: self,
|
||||
state: "closed",
|
||||
label: label
|
||||
)
|
||||
end
|
||||
else
|
||||
create_adjustment(label, order, order)
|
||||
end
|
||||
|
||||
order.adjustments(:reload)
|
||||
order.line_items(:reload)
|
||||
# TaxRate adjustments (order.adjustments.tax)
|
||||
# and price adjustments (tax included on line items) consist of 100% tax
|
||||
(order.adjustments.tax + order.price_adjustments).each do |adjustment|
|
||||
adjustment.set_absolute_included_tax! adjustment.amount
|
||||
end
|
||||
end
|
||||
|
||||
# Manually apply a TaxRate to a particular amount. TaxRates normally compute against
|
||||
# LineItems or Orders, so we mock out a line item here to fit the interface
|
||||
# that our calculator (usually DefaultTax) expects.
|
||||
def compute_tax(amount)
|
||||
line_item = LineItem.new quantity: 1
|
||||
line_item.tax_category = tax_category
|
||||
line_item.define_singleton_method(:price) { amount }
|
||||
|
||||
# Tax on adjustments (represented by the included_tax field) is always inclusive of
|
||||
# tax. However, there's nothing to stop an admin from setting one up with a tax rate
|
||||
# that's marked as not inclusive of tax, and that would result in the DefaultTax
|
||||
# calculator generating a slightly incorrect value. Therefore, we treat the tax
|
||||
# rate as inclusive of tax for the calculations below, regardless of its original
|
||||
# setting.
|
||||
with_tax_included_in_price do
|
||||
calculator.compute line_item
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_label
|
||||
label = ""
|
||||
label << (name.presence || tax_category.name) + " "
|
||||
label << (show_rate_in_label? ? "#{amount * 100}%" : "")
|
||||
end
|
||||
|
||||
def with_tax_included_in_price
|
||||
old_included_in_price = included_in_price
|
||||
|
||||
self.included_in_price = true
|
||||
calculator.calculable.included_in_price = true
|
||||
|
||||
result = yield
|
||||
ensure
|
||||
self.included_in_price = old_included_in_price
|
||||
calculator.calculable.included_in_price = old_included_in_price
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,61 +0,0 @@
|
||||
module Spree
|
||||
TaxRate.class_eval do
|
||||
class << self
|
||||
def match(order)
|
||||
return [] if order.distributor && !order.distributor.charges_sales_tax
|
||||
return [] unless order.tax_zone
|
||||
|
||||
all.select do |rate|
|
||||
rate.zone == order.tax_zone || rate.zone.contains?(order.tax_zone) || rate.zone.default_tax
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def adjust_with_included_tax(order)
|
||||
adjust_without_included_tax(order)
|
||||
|
||||
order.adjustments(:reload)
|
||||
order.line_items(:reload)
|
||||
# TaxRate adjustments (order.adjustments.tax) and price adjustments (tax included on line items) consist of 100% tax
|
||||
(order.adjustments.tax + order.price_adjustments).each do |adjustment|
|
||||
adjustment.set_absolute_included_tax! adjustment.amount
|
||||
end
|
||||
end
|
||||
alias_method_chain :adjust, :included_tax
|
||||
|
||||
# Manually apply a TaxRate to a particular amount. TaxRates normally compute against
|
||||
# LineItems or Orders, so we mock out a line item here to fit the interface
|
||||
# that our calculator (usually DefaultTax) expects.
|
||||
def compute_tax(amount)
|
||||
line_item = LineItem.new quantity: 1
|
||||
line_item.tax_category = tax_category
|
||||
line_item.define_singleton_method(:price) { amount }
|
||||
|
||||
# Tax on adjustments (represented by the included_tax field) is always inclusive of
|
||||
# tax. However, there's nothing to stop an admin from setting one up with a tax rate
|
||||
# that's marked as not inclusive of tax, and that would result in the DefaultTax
|
||||
# calculator generating a slightly incorrect value. Therefore, we treat the tax
|
||||
# rate as inclusive of tax for the calculations below, regardless of its original
|
||||
# setting.
|
||||
with_tax_included_in_price do
|
||||
calculator.compute line_item
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_tax_included_in_price
|
||||
old_included_in_price = included_in_price
|
||||
|
||||
self.included_in_price = true
|
||||
calculator.calculable.included_in_price = true
|
||||
|
||||
result = yield
|
||||
ensure
|
||||
self.included_in_price = old_included_in_price
|
||||
calculator.calculable.included_in_price = old_included_in_price
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
9
spec/factories/return_authorization_factory.rb
Normal file
9
spec/factories/return_authorization_factory.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
FactoryBot.define do
|
||||
factory :return_authorization, class: Spree::ReturnAuthorization do
|
||||
number '100'
|
||||
amount 100.00
|
||||
association(:order, factory: :shipped_order)
|
||||
reason 'no particular reason'
|
||||
state 'received'
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,212 @@ require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Adjustment do
|
||||
let(:order) { build(:order) }
|
||||
let(:adjustment) { Spree::Adjustment.create(label: "Adjustment", amount: 5) }
|
||||
|
||||
describe "scopes" do
|
||||
let!(:arbitrary_adjustment) { create(:adjustment, source: nil, label: "Arbitrary") }
|
||||
let!(:return_authorization_adjustment) { create(:adjustment, source: create(:return_authorization)) }
|
||||
|
||||
it "returns return_authorization adjustments" do
|
||||
expect(Spree::Adjustment.return_authorization.to_a).to eq [return_authorization_adjustment]
|
||||
end
|
||||
end
|
||||
|
||||
context "#update!" do
|
||||
context "when originator present" do
|
||||
let(:originator) { double("originator", update_adjustment: nil) }
|
||||
before do
|
||||
allow(originator).to receive_messages update_amount: true
|
||||
allow(adjustment).to receive_messages originator: originator, label: 'adjustment', amount: 0
|
||||
end
|
||||
|
||||
it "should do nothing when closed" do
|
||||
adjustment.close
|
||||
expect(originator).not_to receive(:update_adjustment)
|
||||
adjustment.update!
|
||||
end
|
||||
|
||||
it "should do nothing when finalized" do
|
||||
adjustment.finalize
|
||||
expect(originator).not_to receive(:update_adjustment)
|
||||
adjustment.update!
|
||||
end
|
||||
|
||||
it "should set the eligibility" do
|
||||
expect(adjustment).to receive(:set_eligibility)
|
||||
adjustment.update!
|
||||
end
|
||||
|
||||
it "should ask the originator to update_adjustment" do
|
||||
expect(originator).to receive(:update_adjustment)
|
||||
adjustment.update!
|
||||
end
|
||||
end
|
||||
|
||||
it "should do nothing when originator is nil" do
|
||||
allow(adjustment).to receive_messages originator: nil
|
||||
expect(adjustment).not_to receive(:amount=)
|
||||
adjustment.update!
|
||||
end
|
||||
end
|
||||
|
||||
context "#eligible? after #set_eligibility" do
|
||||
context "when amount is 0" do
|
||||
before { adjustment.amount = 0 }
|
||||
it "should be eligible if mandatory?" do
|
||||
adjustment.mandatory = true
|
||||
adjustment.set_eligibility
|
||||
expect(adjustment).to be_eligible
|
||||
end
|
||||
|
||||
it "should not be eligible unless mandatory?" do
|
||||
adjustment.mandatory = false
|
||||
adjustment.set_eligibility
|
||||
expect(adjustment).to_not be_eligible
|
||||
end
|
||||
end
|
||||
|
||||
context "when amount is greater than 0" do
|
||||
before { adjustment.amount = 25.00 }
|
||||
|
||||
it "should be eligible if mandatory?" do
|
||||
adjustment.mandatory = true
|
||||
adjustment.set_eligibility
|
||||
expect(adjustment).to be_eligible
|
||||
end
|
||||
|
||||
it "should be eligible if not mandatory and eligible for the originator" do
|
||||
adjustment.mandatory = false
|
||||
allow(adjustment).to receive_messages(eligible_for_originator?: true)
|
||||
adjustment.set_eligibility
|
||||
expect(adjustment).to be_eligible
|
||||
end
|
||||
|
||||
it "should not be eligible if not mandatory not eligible for the originator" do
|
||||
adjustment.mandatory = false
|
||||
allow(adjustment).to receive_messages(eligible_for_originator?: false)
|
||||
adjustment.set_eligibility
|
||||
expect(adjustment).to_not be_eligible
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#save" do
|
||||
it "should call order#update!" do
|
||||
adjustment = Spree::Adjustment.new(
|
||||
adjustable: order,
|
||||
amount: 10,
|
||||
label: "Foo"
|
||||
)
|
||||
expect(order).to receive(:update!)
|
||||
adjustment.save
|
||||
end
|
||||
end
|
||||
|
||||
context "adjustment state" do
|
||||
let(:adjustment) { create(:adjustment, state: 'open') }
|
||||
|
||||
context "#immutable?" do
|
||||
it "is true when adjustment state isn't open" do
|
||||
adjustment.state = "closed"
|
||||
expect(adjustment).to be_immutable
|
||||
adjustment.state = "finalized"
|
||||
expect(adjustment).to be_immutable
|
||||
end
|
||||
|
||||
it "is false when adjustment state is open" do
|
||||
adjustment.state = "open"
|
||||
expect(adjustment).to_not be_immutable
|
||||
end
|
||||
end
|
||||
|
||||
context "#finalized?" do
|
||||
it "is true when adjustment state is finalized" do
|
||||
adjustment.state = "finalized"
|
||||
expect(adjustment).to be_finalized
|
||||
end
|
||||
|
||||
it "is false when adjustment state isn't finalized" do
|
||||
adjustment.state = "closed"
|
||||
expect(adjustment).to_not be_finalized
|
||||
adjustment.state = "open"
|
||||
expect(adjustment).to_not be_finalized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#eligible_for_originator?" do
|
||||
context "with no originator" do
|
||||
specify { expect(adjustment).to be_eligible_for_originator }
|
||||
end
|
||||
|
||||
context "with originator that doesn't have 'eligible?'" do
|
||||
before { adjustment.originator = build(:tax_rate) }
|
||||
specify { expect(adjustment).to be_eligible_for_originator }
|
||||
end
|
||||
|
||||
context "with originator that has 'eligible?'" do
|
||||
let(:originator) { Spree::TaxRate.new }
|
||||
before { adjustment.originator = originator }
|
||||
|
||||
context "and originator is eligible for order" do
|
||||
before { allow(originator).to receive_messages(eligible?: true) }
|
||||
specify { expect(adjustment).to be_eligible_for_originator }
|
||||
end
|
||||
|
||||
context "and originator is not eligible for order" do
|
||||
before { allow(originator).to receive_messages(eligible?: false) }
|
||||
specify { expect(adjustment).to_not be_eligible_for_originator }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#display_amount" do
|
||||
before { adjustment.amount = 10.55 }
|
||||
|
||||
context "with display_currency set to true" do
|
||||
before { Spree::Config[:display_currency] = true }
|
||||
|
||||
it "shows the currency" do
|
||||
expect(adjustment.display_amount.to_s).to eq "$10.55 #{Spree::Config[:currency]}"
|
||||
end
|
||||
end
|
||||
|
||||
context "with display_currency set to false" do
|
||||
before { Spree::Config[:display_currency] = false }
|
||||
|
||||
it "does not include the currency" do
|
||||
expect(adjustment.display_amount.to_s).to eq "$10.55"
|
||||
end
|
||||
end
|
||||
|
||||
context "with currency set to JPY" do
|
||||
context "when adjustable is set to an order" do
|
||||
before do
|
||||
allow(order).to receive(:currency) { 'JPY' }
|
||||
adjustment.adjustable = order
|
||||
end
|
||||
|
||||
it "displays in JPY" do
|
||||
expect(adjustment.display_amount.to_s).to eq "¥11"
|
||||
end
|
||||
end
|
||||
|
||||
context "when adjustable is nil" do
|
||||
it "displays in the default currency" do
|
||||
expect(adjustment.display_amount.to_s).to eq "$10.55"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '#currency' do
|
||||
it 'returns the globally configured currency' do
|
||||
expect(adjustment.currency).to eq Spree::Config[:currency]
|
||||
end
|
||||
end
|
||||
|
||||
it "has metadata" do
|
||||
adjustment = create(:adjustment, metadata: create(:adjustment_metadata))
|
||||
expect(adjustment.metadata).to be
|
||||
@@ -39,7 +245,7 @@ module Spree
|
||||
let!(:zone) { create(:zone_with_member) }
|
||||
let!(:order) { create(:order, bill_address: create(:address)) }
|
||||
let!(:line_item) { create(:line_item, order: order) }
|
||||
let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::FlatRate.new(preferred_amount: 0.1)) }
|
||||
let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: ::Calculator::FlatRate.new(preferred_amount: 0.1)) }
|
||||
let(:adjustment) { line_item.adjustments(:reload).first }
|
||||
|
||||
before do
|
||||
@@ -205,8 +411,10 @@ module Spree
|
||||
end
|
||||
|
||||
context "when enterprise fees inherit their tax_category from the product they are applied to" do
|
||||
let(:product_tax_rate) { create(:tax_rate, included_in_price: true, calculator: ::Calculator::DefaultTax.new, zone: zone, amount: 0.2) }
|
||||
let(:product_tax_category) { create(:tax_category, tax_rates: [product_tax_rate]) }
|
||||
let(:product_tax_rate) {
|
||||
create(:tax_rate, included_in_price: true, calculator: ::Calculator::DefaultTax.new, zone: zone, amount: 0.2)
|
||||
}
|
||||
let(:product_tax_category) { create(:tax_category, tax_rates: [product_tax_rate]) }
|
||||
|
||||
before do
|
||||
variant.product.update_attribute(:tax_category_id, product_tax_category.id)
|
||||
|
||||
30
spec/models/spree/tax_category_spec.rb
Normal file
30
spec/models/spree/tax_category_spec.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::TaxCategory do
|
||||
context 'default tax category' do
|
||||
let(:tax_category) { create(:tax_category) }
|
||||
let(:new_tax_category) { create(:tax_category) }
|
||||
|
||||
before do
|
||||
tax_category.update_column(:is_default, true)
|
||||
end
|
||||
|
||||
it "should undefault the previous default tax category" do
|
||||
new_tax_category.update({ is_default: true })
|
||||
expect(new_tax_category.is_default).to be_truthy
|
||||
|
||||
tax_category.reload
|
||||
expect(tax_category.is_default).to be_falsy
|
||||
end
|
||||
|
||||
it "undefaults the previous default tax category
|
||||
except when updating the existing default tax category" do
|
||||
tax_category.update_column(:description, "Updated description")
|
||||
|
||||
tax_category.reload
|
||||
expect(tax_category.is_default).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user