mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
Merge pull request #5887 from luisramos0/orders
[Bye bye Spree] Bring models order, line_item and other related from spree_core
This commit is contained in:
71
app/models/spree/inventory_unit.rb
Normal file
71
app/models/spree/inventory_unit.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class InventoryUnit < ActiveRecord::Base
|
||||
belongs_to :variant, class_name: "Spree::Variant"
|
||||
belongs_to :order, class_name: "Spree::Order"
|
||||
belongs_to :shipment, class_name: "Spree::Shipment"
|
||||
belongs_to :return_authorization, class_name: "Spree::ReturnAuthorization"
|
||||
|
||||
scope :backordered, -> { where state: 'backordered' }
|
||||
scope :shipped, -> { where state: 'shipped' }
|
||||
scope :backordered_per_variant, ->(stock_item) do
|
||||
includes(:shipment)
|
||||
.where("spree_shipments.state != 'canceled'").references(:shipment)
|
||||
.where(variant_id: stock_item.variant_id)
|
||||
.backordered.order("#{table_name}.created_at ASC")
|
||||
end
|
||||
|
||||
# state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
|
||||
state_machine initial: :on_hand do
|
||||
event :fill_backorder do
|
||||
transition to: :on_hand, from: :backordered
|
||||
end
|
||||
after_transition on: :fill_backorder, do: :update_order
|
||||
|
||||
event :ship do
|
||||
transition to: :shipped, if: :allow_ship?
|
||||
end
|
||||
|
||||
event :return do
|
||||
transition to: :returned, from: :shipped
|
||||
end
|
||||
end
|
||||
|
||||
# This was refactored from a simpler query because the previous implementation
|
||||
# lead to issues once users tried to modify the objects returned. That's due
|
||||
# to ActiveRecord `joins(shipment: :stock_location)` only return readonly
|
||||
# objects
|
||||
#
|
||||
# Returns an array of backordered inventory units as per a given stock item
|
||||
def self.backordered_for_stock_item(stock_item)
|
||||
backordered_per_variant(stock_item).select do |unit|
|
||||
unit.shipment.stock_location == stock_item.stock_location
|
||||
end
|
||||
end
|
||||
|
||||
def self.finalize_units!(inventory_units)
|
||||
inventory_units.map { |iu| iu.update_column(:pending, false) }
|
||||
end
|
||||
|
||||
def find_stock_item
|
||||
Spree::StockItem.find_by(stock_location_id: shipment.stock_location_id,
|
||||
variant_id: variant_id)
|
||||
end
|
||||
|
||||
# Remove variant default_scope `deleted_at: nil`
|
||||
def variant
|
||||
Spree::Variant.unscoped { super }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allow_ship?
|
||||
Spree::Config[:allow_backorder_shipping] || on_hand?
|
||||
end
|
||||
|
||||
def update_order
|
||||
order.update!
|
||||
end
|
||||
end
|
||||
end
|
||||
264
app/models/spree/line_item.rb
Normal file
264
app/models/spree/line_item.rb
Normal file
@@ -0,0 +1,264 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
require 'variant_units/variant_and_line_item_naming'
|
||||
|
||||
module Spree
|
||||
class LineItem < ActiveRecord::Base
|
||||
include VariantUnits::VariantAndLineItemNaming
|
||||
include LineItemBasedAdjustmentHandling
|
||||
|
||||
belongs_to :order, class_name: "Spree::Order", inverse_of: :line_items
|
||||
belongs_to :variant, class_name: "Spree::Variant"
|
||||
belongs_to :tax_category, class_name: "Spree::TaxCategory"
|
||||
|
||||
has_one :product, through: :variant
|
||||
has_many :adjustments, as: :adjustable, dependent: :destroy
|
||||
|
||||
has_and_belongs_to_many :option_values, join_table: 'spree_option_values_line_items',
|
||||
class_name: 'Spree::OptionValue'
|
||||
|
||||
before_validation :adjust_quantity
|
||||
before_validation :copy_price
|
||||
before_validation :copy_tax_category
|
||||
|
||||
validates :variant, presence: true
|
||||
validates :quantity, numericality: {
|
||||
only_integer: true,
|
||||
greater_than: -1,
|
||||
message: Spree.t('validation.must_be_int')
|
||||
}
|
||||
validates :price, numericality: true
|
||||
validates_with Stock::AvailabilityValidator
|
||||
|
||||
before_save :update_inventory
|
||||
before_save :calculate_final_weight_volume, if: :quantity_changed?,
|
||||
unless: :final_weight_volume_changed?
|
||||
after_save :update_order
|
||||
after_save :update_units
|
||||
before_destroy :update_inventory_before_destroy
|
||||
after_destroy :update_order
|
||||
|
||||
delegate :product, :unit_description, :display_name, to: :variant
|
||||
|
||||
attr_accessor :skip_stock_check # Allows manual skipping of Stock::AvailabilityValidator
|
||||
attr_accessor :target_shipment
|
||||
|
||||
# -- Scopes
|
||||
scope :managed_by, lambda { |user|
|
||||
if user.has_spree_role?('admin')
|
||||
where(nil)
|
||||
else
|
||||
# Find line items that are from orders distributed by the user or supplied by the user
|
||||
joins(variant: :product).
|
||||
joins(:order).
|
||||
where('spree_orders.distributor_id IN (?) OR spree_products.supplier_id IN (?)',
|
||||
user.enterprises, user.enterprises).
|
||||
select('spree_line_items.*')
|
||||
end
|
||||
}
|
||||
|
||||
scope :in_orders, lambda { |orders|
|
||||
where(order_id: orders)
|
||||
}
|
||||
|
||||
# Find line items that are from order sorted by variant name and unit value
|
||||
scope :sorted_by_name_and_unit_value, -> {
|
||||
joins(variant: :product).
|
||||
reorder("
|
||||
lower(spree_products.name) asc,
|
||||
lower(spree_variants.display_name) asc,
|
||||
spree_variants.unit_value asc")
|
||||
}
|
||||
|
||||
scope :from_order_cycle, lambda { |order_cycle|
|
||||
joins(order: :order_cycle).
|
||||
where('order_cycles.id = ?', order_cycle)
|
||||
}
|
||||
|
||||
# Here we are simply joining the line item to its variant and product
|
||||
# We dont use joins here to avoid the default scopes,
|
||||
# and with that, include deleted variants and deleted products
|
||||
scope :supplied_by_any, lambda { |enterprises|
|
||||
product_ids = Spree::Product.unscoped.where(supplier_id: enterprises).select(:id)
|
||||
variant_ids = Spree::Variant.unscoped.where(product_id: product_ids).select(:id)
|
||||
where("spree_line_items.variant_id IN (?)", variant_ids)
|
||||
}
|
||||
|
||||
scope :with_tax, -> {
|
||||
joins(:adjustments).
|
||||
where('spree_adjustments.originator_type = ?', 'Spree::TaxRate').
|
||||
select('DISTINCT spree_line_items.*')
|
||||
}
|
||||
|
||||
# Line items without a Spree::TaxRate-originated adjustment
|
||||
scope :without_tax, -> {
|
||||
joins("
|
||||
LEFT OUTER JOIN spree_adjustments
|
||||
ON (spree_adjustments.adjustable_id=spree_line_items.id
|
||||
AND spree_adjustments.adjustable_type = 'Spree::LineItem'
|
||||
AND spree_adjustments.originator_type='Spree::TaxRate')").
|
||||
where('spree_adjustments.id IS NULL')
|
||||
}
|
||||
|
||||
def copy_price
|
||||
return unless variant
|
||||
|
||||
self.price = variant.price if price.nil?
|
||||
self.cost_price = variant.cost_price if cost_price.nil?
|
||||
self.currency = variant.currency if currency.nil?
|
||||
end
|
||||
|
||||
def copy_tax_category
|
||||
return unless variant
|
||||
|
||||
self.tax_category = variant.product.tax_category
|
||||
end
|
||||
|
||||
def amount
|
||||
price * quantity
|
||||
end
|
||||
alias total amount
|
||||
|
||||
def single_money
|
||||
Spree::Money.new(price, currency: currency)
|
||||
end
|
||||
alias single_display_amount single_money
|
||||
|
||||
def money
|
||||
Spree::Money.new(amount, currency: currency)
|
||||
end
|
||||
alias display_total money
|
||||
alias display_amount money
|
||||
|
||||
def adjust_quantity
|
||||
self.quantity = 0 if quantity.nil? || quantity < 0
|
||||
end
|
||||
|
||||
# Here we skip stock check if skip_stock_check flag is active,
|
||||
# we skip stock check if requested quantity is zero or negative,
|
||||
# and we scope variants to hub and thus acivate variant overrides.
|
||||
def sufficient_stock?
|
||||
return true if skip_stock_check
|
||||
return true if quantity <= 0
|
||||
|
||||
scoper.scope(variant)
|
||||
variant.can_supply?(quantity)
|
||||
end
|
||||
|
||||
def insufficient_stock?
|
||||
!sufficient_stock?
|
||||
end
|
||||
|
||||
def assign_stock_changes_to=(shipment)
|
||||
@preferred_shipment = shipment
|
||||
end
|
||||
|
||||
# Remove product default_scope `deleted_at: nil`
|
||||
def product
|
||||
variant.product
|
||||
end
|
||||
|
||||
# This ensures that LineItems always have access to soft-deleted variants.
|
||||
# In some situations, unscoped super will be nil. In these cases,
|
||||
# we fetch the variant using variant_id. See issue #4946 for more details.
|
||||
def variant
|
||||
Spree::Variant.unscoped { super } || Spree::Variant.unscoped.find(variant_id)
|
||||
end
|
||||
|
||||
def cap_quantity_at_stock!
|
||||
scoper.scope(variant)
|
||||
return if variant.on_demand
|
||||
|
||||
update!(quantity: variant.on_hand) if quantity > variant.on_hand
|
||||
end
|
||||
|
||||
def has_tax?
|
||||
adjustments.included_tax.any?
|
||||
end
|
||||
|
||||
def included_tax
|
||||
adjustments.included_tax.sum(&:included_tax)
|
||||
end
|
||||
|
||||
def tax_rates
|
||||
product.tax_category.andand.tax_rates || []
|
||||
end
|
||||
|
||||
def price_with_adjustments
|
||||
# EnterpriseFee#create_adjustment applies adjustments on line items to their parent order,
|
||||
# so line_item.adjustments returns an empty array
|
||||
return 0 if quantity.zero?
|
||||
|
||||
line_item_adjustments = OrderAdjustmentsFetcher.new(order).line_item_adjustments(self)
|
||||
|
||||
(price + line_item_adjustments.sum(&:amount) / quantity).round(2)
|
||||
end
|
||||
|
||||
def single_display_amount_with_adjustments
|
||||
Spree::Money.new(price_with_adjustments, currency: currency)
|
||||
end
|
||||
|
||||
def amount_with_adjustments
|
||||
# We calculate from price_with_adjustments here rather than building our own value because
|
||||
# rounding errors can produce discrepencies of $0.01.
|
||||
price_with_adjustments * quantity
|
||||
end
|
||||
|
||||
def display_amount_with_adjustments
|
||||
Spree::Money.new(amount_with_adjustments, currency: currency)
|
||||
end
|
||||
|
||||
def display_included_tax
|
||||
Spree::Money.new(included_tax, currency: currency)
|
||||
end
|
||||
|
||||
def unit_value
|
||||
return variant.unit_value if quantity == 0 || !final_weight_volume
|
||||
|
||||
final_weight_volume / quantity
|
||||
end
|
||||
|
||||
def scoper
|
||||
@scoper ||= OpenFoodNetwork::ScopeVariantToHub.new(order.distributor)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_inventory
|
||||
return unless changed?
|
||||
|
||||
scoper.scope(variant)
|
||||
Spree::OrderInventory.new(order).verify(self, target_shipment)
|
||||
end
|
||||
|
||||
def update_order
|
||||
return unless changed? || destroyed?
|
||||
|
||||
# update the order totals, etc.
|
||||
order.create_tax_charge!
|
||||
order.update!
|
||||
end
|
||||
|
||||
def update_inventory_before_destroy
|
||||
# This is necessary before destroying the line item
|
||||
# so that update_inventory will restore stock to the variant
|
||||
self.quantity = 0
|
||||
|
||||
update_inventory
|
||||
|
||||
# This is necessary after updating inventory
|
||||
# because update_inventory may delete the last shipment in the order
|
||||
# and that makes update_order fail if we don't reload the shipments
|
||||
order.shipments.reload
|
||||
end
|
||||
|
||||
def calculate_final_weight_volume
|
||||
if final_weight_volume.present? && quantity_was > 0
|
||||
self.final_weight_volume = final_weight_volume * quantity / quantity_was
|
||||
elsif variant.andand.unit_value.present?
|
||||
self.final_weight_volume = variant.andand.unit_value * quantity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,185 +0,0 @@
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
require 'variant_units/variant_and_line_item_naming'
|
||||
|
||||
Spree::LineItem.class_eval do
|
||||
include VariantUnits::VariantAndLineItemNaming
|
||||
include LineItemBasedAdjustmentHandling
|
||||
has_and_belongs_to_many :option_values, join_table: 'spree_option_values_line_items', class_name: 'Spree::OptionValue'
|
||||
|
||||
# Redefining here to add the inverse_of option
|
||||
belongs_to :order, class_name: "Spree::Order", inverse_of: :line_items
|
||||
|
||||
# Allows manual skipping of Stock::AvailabilityValidator
|
||||
attr_accessor :skip_stock_check
|
||||
|
||||
before_save :calculate_final_weight_volume, if: :quantity_changed?, unless: :final_weight_volume_changed?
|
||||
after_save :update_units
|
||||
|
||||
before_destroy :update_inventory_before_destroy
|
||||
|
||||
delegate :product, :unit_description, to: :variant
|
||||
|
||||
# -- Scopes
|
||||
scope :managed_by, lambda { |user|
|
||||
if user.has_spree_role?('admin')
|
||||
where(nil)
|
||||
else
|
||||
# Find line items that are from orders distributed by the user or supplied by the user
|
||||
joins(variant: :product).
|
||||
joins(:order).
|
||||
where('spree_orders.distributor_id IN (?) OR spree_products.supplier_id IN (?)', user.enterprises, user.enterprises).
|
||||
select('spree_line_items.*')
|
||||
end
|
||||
}
|
||||
|
||||
scope :in_orders, lambda { |orders|
|
||||
where(order_id: orders)
|
||||
}
|
||||
|
||||
# Find line items that are from order sorted by variant name and unit value
|
||||
scope :sorted_by_name_and_unit_value, -> {
|
||||
joins(variant: :product).
|
||||
reorder("
|
||||
lower(spree_products.name) asc,
|
||||
lower(spree_variants.display_name) asc,
|
||||
spree_variants.unit_value asc")
|
||||
}
|
||||
|
||||
scope :from_order_cycle, lambda { |order_cycle|
|
||||
joins(order: :order_cycle).
|
||||
where('order_cycles.id = ?', order_cycle)
|
||||
}
|
||||
|
||||
# Here we are simply joining the line item to its variant and product
|
||||
# We dont use joins here to avoid the default scopes,
|
||||
# and with that, include deleted variants and deleted products
|
||||
scope :supplied_by_any, lambda { |enterprises|
|
||||
product_ids = Spree::Product.unscoped.where(supplier_id: enterprises).select(:id)
|
||||
variant_ids = Spree::Variant.unscoped.where(product_id: product_ids).select(:id)
|
||||
where("spree_line_items.variant_id IN (?)", variant_ids)
|
||||
}
|
||||
|
||||
scope :with_tax, -> {
|
||||
joins(:adjustments).
|
||||
where('spree_adjustments.originator_type = ?', 'Spree::TaxRate').
|
||||
select('DISTINCT spree_line_items.*')
|
||||
}
|
||||
|
||||
# Line items without a Spree::TaxRate-originated adjustment
|
||||
scope :without_tax, -> {
|
||||
joins("
|
||||
LEFT OUTER JOIN spree_adjustments
|
||||
ON (spree_adjustments.adjustable_id=spree_line_items.id
|
||||
AND spree_adjustments.adjustable_type = 'Spree::LineItem'
|
||||
AND spree_adjustments.originator_type='Spree::TaxRate')").
|
||||
where('spree_adjustments.id IS NULL')
|
||||
}
|
||||
|
||||
# Overridden so that LineItems always have access to soft-deleted Variant
|
||||
# attributes. In some situations, unscoped super will be nil, in these cases
|
||||
# we fetch the variant using the variant_id. See isssue #4946 for more
|
||||
# details
|
||||
def variant
|
||||
Spree::Variant.unscoped { super } || Spree::Variant.unscoped.find(variant_id)
|
||||
end
|
||||
|
||||
def cap_quantity_at_stock!
|
||||
scoper.scope(variant)
|
||||
return if variant.on_demand
|
||||
|
||||
update!(quantity: variant.on_hand) if quantity > variant.on_hand
|
||||
end
|
||||
|
||||
def has_tax?
|
||||
adjustments.included_tax.any?
|
||||
end
|
||||
|
||||
def included_tax
|
||||
adjustments.included_tax.sum(&:included_tax)
|
||||
end
|
||||
|
||||
def tax_rates
|
||||
product.tax_category.andand.tax_rates || []
|
||||
end
|
||||
|
||||
def price_with_adjustments
|
||||
# EnterpriseFee#create_adjustment applies adjustments on line items to their parent order,
|
||||
# so line_item.adjustments returns an empty array
|
||||
return 0 if quantity.zero?
|
||||
|
||||
line_item_adjustments = OrderAdjustmentsFetcher.new(order).line_item_adjustments(self)
|
||||
|
||||
(price + line_item_adjustments.sum(&:amount) / quantity).round(2)
|
||||
end
|
||||
|
||||
def single_display_amount_with_adjustments
|
||||
Spree::Money.new(price_with_adjustments, currency: currency)
|
||||
end
|
||||
|
||||
def amount_with_adjustments
|
||||
# We calculate from price_with_adjustments here rather than building our own value because
|
||||
# rounding errors can produce discrepencies of $0.01.
|
||||
price_with_adjustments * quantity
|
||||
end
|
||||
|
||||
def display_amount_with_adjustments
|
||||
Spree::Money.new(amount_with_adjustments, currency: currency)
|
||||
end
|
||||
|
||||
def display_included_tax
|
||||
Spree::Money.new(included_tax, currency: currency)
|
||||
end
|
||||
|
||||
delegate :display_name, to: :variant
|
||||
|
||||
def unit_value
|
||||
return variant.unit_value if quantity == 0 || !final_weight_volume
|
||||
|
||||
final_weight_volume / quantity
|
||||
end
|
||||
|
||||
# Overrides Spree version to:
|
||||
# - skip stock check if skip_stock_check flag is active
|
||||
# - skip stock check if requested quantity is zero or negative
|
||||
# - scope variants to hub and thus acivate variant overrides
|
||||
def sufficient_stock?
|
||||
return true if skip_stock_check
|
||||
return true if quantity <= 0
|
||||
|
||||
scoper.scope(variant)
|
||||
variant.can_supply?(quantity)
|
||||
end
|
||||
|
||||
def scoper
|
||||
@scoper ||= OpenFoodNetwork::ScopeVariantToHub.new(order.distributor)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_inventory_with_scoping
|
||||
scoper.scope(variant)
|
||||
update_inventory_without_scoping
|
||||
end
|
||||
alias_method_chain :update_inventory, :scoping
|
||||
|
||||
def update_inventory_before_destroy
|
||||
# This is necessary before destroying the line item
|
||||
# so that update_inventory will restore stock to the variant
|
||||
self.quantity = 0
|
||||
|
||||
update_inventory
|
||||
|
||||
# This is necessary after updating inventory
|
||||
# because update_inventory may delete the last shipment in the order
|
||||
# and that makes update_order fail if we don't reload the shipments
|
||||
order.shipments.reload
|
||||
end
|
||||
|
||||
def calculate_final_weight_volume
|
||||
if final_weight_volume.present? && quantity_was > 0
|
||||
self.final_weight_volume = final_weight_volume * quantity / quantity_was
|
||||
elsif variant.andand.unit_value.present?
|
||||
self.final_weight_volume = variant.andand.unit_value * quantity
|
||||
end
|
||||
end
|
||||
end
|
||||
914
app/models/spree/order.rb
Normal file
914
app/models/spree/order.rb
Normal file
@@ -0,0 +1,914 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spree/order/checkout'
|
||||
require 'open_food_network/enterprise_fee_calculator'
|
||||
require 'open_food_network/feature_toggle'
|
||||
require 'open_food_network/tag_rule_applicator'
|
||||
require 'concerns/order_shipment'
|
||||
|
||||
ActiveSupport::Notifications
|
||||
.subscribe('spree.order.contents_changed') do |_name, _start, _finish, _id, payload|
|
||||
payload[:order].reload.update_distribution_charge!
|
||||
end
|
||||
|
||||
module Spree
|
||||
class Order < ActiveRecord::Base
|
||||
prepend OrderShipment
|
||||
include Checkout
|
||||
|
||||
checkout_flow do
|
||||
go_to_state :address
|
||||
go_to_state :delivery
|
||||
go_to_state :payment, if: ->(order) {
|
||||
order.update_totals
|
||||
order.payment_required?
|
||||
}
|
||||
go_to_state :complete
|
||||
end
|
||||
|
||||
state_machine.after_transition to: :payment, do: :charge_shipping_and_payment_fees!
|
||||
|
||||
state_machine.event :restart_checkout do
|
||||
transition to: :cart, unless: :completed?
|
||||
end
|
||||
|
||||
token_resource
|
||||
|
||||
belongs_to :user, class_name: Spree.user_class.to_s
|
||||
belongs_to :created_by, class_name: Spree.user_class.to_s
|
||||
|
||||
belongs_to :bill_address, foreign_key: :bill_address_id, class_name: 'Spree::Address'
|
||||
alias_attribute :billing_address, :bill_address
|
||||
|
||||
belongs_to :ship_address, foreign_key: :ship_address_id, class_name: 'Spree::Address'
|
||||
alias_attribute :shipping_address, :ship_address
|
||||
|
||||
has_many :state_changes, as: :stateful
|
||||
has_many :line_items, -> { order('created_at ASC') }, dependent: :destroy
|
||||
has_many :payments, dependent: :destroy
|
||||
has_many :return_authorizations, dependent: :destroy
|
||||
has_many :adjustments, -> { order "#{Spree::Adjustment.table_name}.created_at ASC" },
|
||||
as: :adjustable,
|
||||
dependent: :destroy
|
||||
|
||||
has_many :line_item_adjustments, through: :line_items, source: :adjustments
|
||||
|
||||
has_many :shipments, dependent: :destroy do
|
||||
def states
|
||||
pluck(:state).uniq
|
||||
end
|
||||
end
|
||||
|
||||
belongs_to :order_cycle
|
||||
belongs_to :distributor, class_name: 'Enterprise'
|
||||
belongs_to :customer
|
||||
has_one :proxy_order
|
||||
has_one :subscription, through: :proxy_order
|
||||
|
||||
accepts_nested_attributes_for :line_items
|
||||
accepts_nested_attributes_for :bill_address
|
||||
accepts_nested_attributes_for :ship_address
|
||||
accepts_nested_attributes_for :payments
|
||||
accepts_nested_attributes_for :shipments
|
||||
|
||||
delegate :admin_and_handling_total, :payment_fee, :ship_total, to: :adjustments_fetcher
|
||||
|
||||
# Needs to happen before save_permalink is called
|
||||
before_validation :set_currency
|
||||
before_validation :generate_order_number, on: :create
|
||||
before_validation :clone_billing_address, if: :use_billing?
|
||||
before_validation :associate_customer, unless: :customer_id?
|
||||
before_validation :ensure_customer, unless: :customer_is_valid?
|
||||
|
||||
validates :customer, presence: true, if: :require_customer?
|
||||
validate :products_available_from_new_distribution, if: lambda {
|
||||
distributor_id_changed? || order_cycle_id_changed?
|
||||
}
|
||||
validate :disallow_guest_order
|
||||
|
||||
attr_accessor :use_billing
|
||||
|
||||
before_create :link_by_email
|
||||
after_create :create_tax_charge!
|
||||
|
||||
validate :has_available_shipment
|
||||
validate :has_available_payment
|
||||
validates :email, presence: true,
|
||||
format: /\A([\w\.%\+\-']+)@([\w\-]+\.)+([\w]{2,})\z/i,
|
||||
if: :require_email
|
||||
|
||||
make_permalink field: :number
|
||||
|
||||
before_save :update_shipping_fees!, if: :complete?
|
||||
before_save :update_payment_fees!, if: :complete?
|
||||
|
||||
class_attribute :update_hooks
|
||||
self.update_hooks = Set.new
|
||||
|
||||
# -- Scopes
|
||||
scope :managed_by, lambda { |user|
|
||||
if user.has_spree_role?('admin')
|
||||
where(nil)
|
||||
else
|
||||
# Find orders that are distributed by the user or have products supplied by the user
|
||||
# WARNING: This only filters orders,
|
||||
# you'll need to filter line items separately using LineItem.managed_by
|
||||
with_line_items_variants_and_products_outer.
|
||||
where('spree_orders.distributor_id IN (?) OR spree_products.supplier_id IN (?)',
|
||||
user.enterprises.select(&:id),
|
||||
user.enterprises.select(&:id)).
|
||||
select('DISTINCT spree_orders.*')
|
||||
end
|
||||
}
|
||||
|
||||
scope :distributed_by_user, lambda { |user|
|
||||
if user.has_spree_role?('admin')
|
||||
where(nil)
|
||||
else
|
||||
where('spree_orders.distributor_id IN (?)', user.enterprises.select(&:id))
|
||||
end
|
||||
}
|
||||
|
||||
scope :with_line_items_variants_and_products_outer, lambda {
|
||||
joins('LEFT OUTER JOIN spree_line_items ON (spree_line_items.order_id = spree_orders.id)').
|
||||
joins('LEFT OUTER JOIN spree_variants ON (spree_variants.id = spree_line_items.variant_id)').
|
||||
joins('LEFT OUTER JOIN spree_products ON (spree_products.id = spree_variants.product_id)')
|
||||
}
|
||||
|
||||
scope :not_state, lambda { |state|
|
||||
where("state != ?", state)
|
||||
}
|
||||
|
||||
def self.by_number(number)
|
||||
where(number: number)
|
||||
end
|
||||
|
||||
def self.between(start_date, end_date)
|
||||
where(created_at: start_date..end_date)
|
||||
end
|
||||
|
||||
def self.by_customer(customer)
|
||||
joins(:user).where("#{Spree.user_class.table_name}.email" => customer)
|
||||
end
|
||||
|
||||
def self.by_state(state)
|
||||
where(state: state)
|
||||
end
|
||||
|
||||
def self.complete
|
||||
where('completed_at IS NOT NULL')
|
||||
end
|
||||
|
||||
def self.incomplete
|
||||
where(completed_at: nil)
|
||||
end
|
||||
|
||||
# Use this method in other gems that wish to register their own custom logic
|
||||
# that should be called after Order#update
|
||||
def self.register_update_hook(hook)
|
||||
update_hooks.add(hook)
|
||||
end
|
||||
|
||||
# For compatiblity with Calculator::PriceSack
|
||||
def amount
|
||||
line_items.inject(0.0) { |sum, li| sum + li.amount }
|
||||
end
|
||||
|
||||
def currency
|
||||
self[:currency] || Spree::Config[:currency]
|
||||
end
|
||||
|
||||
def display_outstanding_balance
|
||||
Spree::Money.new(outstanding_balance, currency: currency)
|
||||
end
|
||||
|
||||
def display_item_total
|
||||
Spree::Money.new(item_total, currency: currency)
|
||||
end
|
||||
|
||||
def display_adjustment_total
|
||||
Spree::Money.new(adjustment_total, currency: currency)
|
||||
end
|
||||
|
||||
def display_tax_total
|
||||
Spree::Money.new(tax_total, currency: currency)
|
||||
end
|
||||
|
||||
def display_ship_total
|
||||
Spree::Money.new(ship_total, currency: currency)
|
||||
end
|
||||
|
||||
def display_total
|
||||
Spree::Money.new(total, currency: currency)
|
||||
end
|
||||
|
||||
def to_param
|
||||
number.to_s.to_url.upcase
|
||||
end
|
||||
|
||||
def completed?
|
||||
completed_at.present?
|
||||
end
|
||||
|
||||
# Indicates whether or not the user is allowed to proceed to checkout.
|
||||
# Currently this is implemented as a check for whether or not there is at
|
||||
# least one LineItem in the Order. Feel free to override this logic in your
|
||||
# own application if you require additional steps before allowing a checkout.
|
||||
def checkout_allowed?
|
||||
line_items.count > 0
|
||||
end
|
||||
|
||||
def changes_allowed?
|
||||
complete? && distributor.andand.allow_order_changes? && order_cycle.andand.open?
|
||||
end
|
||||
|
||||
# Is this a free order in which case the payment step should be skipped
|
||||
# This allows unpaid subscription orders to be completed.
|
||||
# Subscriptions place orders at the beginning of an order cycle. They need to
|
||||
# be completed to draw from stock levels and trigger emails.
|
||||
def payment_required?
|
||||
total.to_f > 0.0 && !skip_payment_for_subscription?
|
||||
end
|
||||
|
||||
# Indicates the number of items in the order
|
||||
def item_count
|
||||
line_items.inject(0) { |sum, li| sum + li.quantity }
|
||||
end
|
||||
|
||||
def backordered?
|
||||
shipments.any?(&:backordered?)
|
||||
end
|
||||
|
||||
# Returns the relevant zone (if any) to be used for taxation purposes.
|
||||
# Uses default tax zone unless there is a specific match
|
||||
def tax_zone
|
||||
Zone.match(tax_address) || Zone.default_tax
|
||||
end
|
||||
|
||||
# Indicates whether tax should be backed out of the price calcualtions in
|
||||
# cases where prices include tax but the customer is not required to pay
|
||||
# taxes in that case.
|
||||
def exclude_tax?
|
||||
return false unless Spree::Config[:prices_inc_tax]
|
||||
|
||||
tax_zone != Zone.default_tax
|
||||
end
|
||||
|
||||
# Returns the address for taxation based on configuration
|
||||
def tax_address
|
||||
Spree::Config[:tax_using_ship_address] ? ship_address : bill_address
|
||||
end
|
||||
|
||||
# Array of totals grouped by Adjustment#label. Useful for displaying line item
|
||||
# adjustments on an invoice. For example, you can display tax breakout for
|
||||
# cases where tax is included in price.
|
||||
def line_item_adjustment_totals
|
||||
Hash[line_item_adjustments.eligible.group_by(&:label).map do |label, adjustments|
|
||||
total = adjustments.sum(&:amount)
|
||||
[label, Spree::Money.new(total, currency: currency)]
|
||||
end]
|
||||
end
|
||||
|
||||
def updater
|
||||
@updater ||= OrderManagement::Order::Updater.new(self)
|
||||
end
|
||||
|
||||
def update!
|
||||
updater.update
|
||||
end
|
||||
|
||||
delegate :update_totals, to: :updater
|
||||
|
||||
def clone_billing_address
|
||||
if bill_address && ship_address.nil?
|
||||
self.ship_address = bill_address.clone
|
||||
else
|
||||
ship_address.attributes = bill_address.attributes.except('id', 'updated_at', 'created_at')
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def allow_cancel?
|
||||
return false unless completed? && (state != 'canceled')
|
||||
|
||||
shipment_state.nil? || %w{ready backorder pending}.include?(shipment_state)
|
||||
end
|
||||
|
||||
def allow_resume?
|
||||
# we shouldn't allow resume for legacy orders b/c we lack the information
|
||||
# necessary to restore to a previous state
|
||||
return false if state_changes.empty? || state_changes.last.previous_state.nil?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def awaiting_returns?
|
||||
return_authorizations.any?(&:authorized?)
|
||||
end
|
||||
|
||||
# This is currently used when adding a variant to an order in the BackOffice.
|
||||
# Spree::OrderContents#add is equivalent but slightly different from add_variant below.
|
||||
def contents
|
||||
@contents ||= Spree::OrderContents.new(self)
|
||||
end
|
||||
|
||||
# This is currently used when adding a variant to an order in the FrontOffice.
|
||||
# This add_variant is equivalent but slightly different from Spree::OrderContents#add above.
|
||||
# Spree::OrderContents#add is the more modern version in Spree history
|
||||
# but this add_variant has been customized for OFN FrontOffice.
|
||||
def add_variant(variant, quantity = 1, max_quantity = nil, currency = nil)
|
||||
line_items(:reload)
|
||||
current_item = find_line_item_by_variant(variant)
|
||||
|
||||
# Notify bugsnag if we get line items with a quantity of zero
|
||||
if quantity == 0
|
||||
Bugsnag.notify(RuntimeError.new("Zero Quantity Line Item"),
|
||||
current_item: current_item.as_json,
|
||||
line_items: line_items.map(&:id),
|
||||
variant: variant.as_json)
|
||||
end
|
||||
|
||||
if current_item
|
||||
current_item.quantity = quantity
|
||||
current_item.max_quantity = max_quantity
|
||||
|
||||
current_item.currency = currency unless currency.nil?
|
||||
current_item.save
|
||||
else
|
||||
current_item = Spree::LineItem.new(quantity: quantity, max_quantity: max_quantity)
|
||||
current_item.variant = variant
|
||||
if currency
|
||||
current_item.currency = currency unless currency.nil?
|
||||
current_item.price = variant.price_in(currency).amount
|
||||
else
|
||||
current_item.price = variant.price
|
||||
end
|
||||
line_items << current_item
|
||||
end
|
||||
|
||||
reload
|
||||
current_item
|
||||
end
|
||||
|
||||
def set_variant_attributes(variant, attributes)
|
||||
line_item = find_line_item_by_variant(variant)
|
||||
|
||||
return unless line_item
|
||||
|
||||
if attributes.key?(:max_quantity) && attributes[:max_quantity].to_i < line_item.quantity
|
||||
attributes[:max_quantity] = line_item.quantity
|
||||
end
|
||||
|
||||
line_item.assign_attributes(attributes)
|
||||
line_item.save!
|
||||
end
|
||||
|
||||
# Associates the specified user with the order.
|
||||
def associate_user!(user)
|
||||
self.user = user
|
||||
self.email = user.email
|
||||
self.created_by = user if created_by.blank?
|
||||
|
||||
return unless persisted?
|
||||
|
||||
# Persist the changes we just made,
|
||||
# but don't use save since we might have an invalid address associated
|
||||
self.class.unscoped.where(id: id).update_all(email: user.email,
|
||||
user_id: user.id,
|
||||
created_by_id: created_by_id)
|
||||
end
|
||||
|
||||
# FIXME refactor this method and implement validation using validates_* utilities
|
||||
def generate_order_number
|
||||
record = true
|
||||
while record
|
||||
random = "R#{Array.new(9){ rand(9) }.join}"
|
||||
record = self.class.find_by(number: random)
|
||||
end
|
||||
self.number = random if number.blank?
|
||||
number
|
||||
end
|
||||
|
||||
def shipped_shipments
|
||||
shipments.shipped
|
||||
end
|
||||
|
||||
def contains?(variant)
|
||||
find_line_item_by_variant(variant).present?
|
||||
end
|
||||
|
||||
def find_line_item_by_variant(variant)
|
||||
line_items.detect { |line_item| line_item.variant_id == variant.id }
|
||||
end
|
||||
|
||||
def ship_total
|
||||
adjustments.shipping.map(&:amount).sum
|
||||
end
|
||||
|
||||
def tax_total
|
||||
adjustments.tax.map(&:amount).sum
|
||||
end
|
||||
|
||||
# Creates new tax charges if there are any applicable rates. If prices already
|
||||
# include taxes then price adjustments are created instead.
|
||||
def create_tax_charge!
|
||||
Spree::TaxRate.adjust(self)
|
||||
end
|
||||
|
||||
def outstanding_balance
|
||||
total - payment_total
|
||||
end
|
||||
|
||||
def outstanding_balance?
|
||||
outstanding_balance != 0
|
||||
end
|
||||
|
||||
def name
|
||||
address = bill_address || ship_address
|
||||
return unless address
|
||||
|
||||
"#{address.firstname} #{address.lastname}"
|
||||
end
|
||||
|
||||
def can_ship?
|
||||
complete? || resumed? || awaiting_return? || returned?
|
||||
end
|
||||
|
||||
def credit_cards
|
||||
credit_card_ids = payments.from_credit_card.pluck(:source_id).uniq
|
||||
CreditCard.where(id: credit_card_ids)
|
||||
end
|
||||
|
||||
# Finalizes an in progress order after checkout is complete.
|
||||
# Called after transition to complete state when payments will have been processed
|
||||
def finalize!
|
||||
touch :completed_at
|
||||
|
||||
adjustments.update_all state: 'closed'
|
||||
|
||||
# update payment and shipment(s) states, and save
|
||||
updater.update_payment_state
|
||||
shipments.each do |shipment|
|
||||
shipment.update!(self)
|
||||
shipment.finalize!
|
||||
end
|
||||
|
||||
updater.update_shipment_state
|
||||
updater.before_save_hook
|
||||
save
|
||||
updater.run_hooks
|
||||
|
||||
deliver_order_confirmation_email
|
||||
|
||||
state_changes.create(
|
||||
previous_state: 'cart',
|
||||
next_state: 'complete',
|
||||
name: 'order',
|
||||
user_id: user_id
|
||||
)
|
||||
end
|
||||
|
||||
def deliver_order_confirmation_email
|
||||
return if subscription.present?
|
||||
|
||||
Delayed::Job.enqueue ConfirmOrderJob.new(id)
|
||||
end
|
||||
|
||||
# Helper methods for checkout steps
|
||||
def paid?
|
||||
payment_state == 'paid' || payment_state == 'credit_owed'
|
||||
end
|
||||
|
||||
def available_payment_methods
|
||||
@available_payment_methods ||= PaymentMethod.available(:front_end)
|
||||
end
|
||||
|
||||
# "Checkout" is the initial state and, for card payments, "pending" is the state after auth
|
||||
# These are both valid states to process the payment
|
||||
def pending_payments
|
||||
(payments.select(&:pending?) +
|
||||
payments.select(&:processing?) +
|
||||
payments.select(&:checkout?)).uniq
|
||||
end
|
||||
|
||||
# processes any pending payments and must return a boolean as it's
|
||||
# return value is used by the checkout state_machine to determine
|
||||
# success or failure of the 'complete' event for the order
|
||||
#
|
||||
# Returns:
|
||||
# - true if all pending_payments processed successfully
|
||||
# - true if a payment failed, ie. raised a GatewayError
|
||||
# which gets rescued and converted to TRUE when
|
||||
# :allow_checkout_gateway_error is set to true
|
||||
# - false if a payment failed, ie. raised a GatewayError
|
||||
# which gets rescued and converted to FALSE when
|
||||
# :allow_checkout_on_gateway_error is set to false
|
||||
#
|
||||
def process_payments!
|
||||
raise Core::GatewayError, Spree.t(:no_pending_payments) if pending_payments.empty?
|
||||
|
||||
pending_payments.each do |payment|
|
||||
break if payment_total >= total
|
||||
|
||||
payment.process!
|
||||
|
||||
if payment.completed?
|
||||
self.payment_total += payment.amount
|
||||
end
|
||||
end
|
||||
rescue Core::GatewayError => e
|
||||
result = !!Spree::Config[:allow_checkout_on_gateway_error]
|
||||
errors.add(:base, e.message) && (return result)
|
||||
end
|
||||
|
||||
def billing_firstname
|
||||
bill_address.try(:firstname)
|
||||
end
|
||||
|
||||
def billing_lastname
|
||||
bill_address.try(:lastname)
|
||||
end
|
||||
|
||||
def products
|
||||
line_items.map(&:product)
|
||||
end
|
||||
|
||||
def variants
|
||||
line_items.map(&:variant)
|
||||
end
|
||||
|
||||
def insufficient_stock_lines
|
||||
line_items.select(&:insufficient_stock?)
|
||||
end
|
||||
|
||||
def empty!
|
||||
line_items.destroy_all
|
||||
adjustments.destroy_all
|
||||
payments.clear
|
||||
shipments.destroy_all
|
||||
end
|
||||
|
||||
def clear_adjustments!
|
||||
adjustments.destroy_all
|
||||
line_item_adjustments.destroy_all
|
||||
end
|
||||
|
||||
def has_step?(step)
|
||||
checkout_steps.include?(step)
|
||||
end
|
||||
|
||||
def state_changed(name)
|
||||
state = "#{name}_state"
|
||||
return unless persisted?
|
||||
|
||||
old_state = __send__("#{state}_was")
|
||||
state_changes.create(
|
||||
previous_state: old_state,
|
||||
next_state: __send__(state),
|
||||
name: name,
|
||||
user_id: user_id
|
||||
)
|
||||
end
|
||||
|
||||
def shipped?
|
||||
%w(partial shipped).include?(shipment_state)
|
||||
end
|
||||
|
||||
# Does this order have shipments that can be shipped?
|
||||
def ready_to_ship?
|
||||
shipments.any?(&:can_ship?)
|
||||
end
|
||||
|
||||
# Ship all pending orders
|
||||
def ship
|
||||
shipments.each do |s|
|
||||
s.ship if s.can_ship?
|
||||
end
|
||||
end
|
||||
|
||||
def line_item_variants
|
||||
if line_items.loaded?
|
||||
line_items.map(&:variant)
|
||||
else
|
||||
line_items.includes(:variant).map(&:variant)
|
||||
end
|
||||
end
|
||||
|
||||
# Show already bought line items of this order cycle
|
||||
def finalised_line_items
|
||||
return [] unless order_cycle && user && distributor
|
||||
|
||||
order_cycle.items_bought_by_user(user, distributor)
|
||||
end
|
||||
|
||||
def create_proposed_shipments
|
||||
adjustments.shipping.delete_all
|
||||
shipments.destroy_all
|
||||
|
||||
packages = OrderManagement::Stock::Coordinator.new(self).packages
|
||||
packages.each do |package|
|
||||
shipments << package.to_shipment
|
||||
end
|
||||
|
||||
shipments
|
||||
end
|
||||
|
||||
# Clean shipments and make order back to address state
|
||||
#
|
||||
# At some point the might need to force the order to transition from address
|
||||
# to delivery again so that proper updated shipments are created.
|
||||
# e.g. customer goes back from payment step and changes order items
|
||||
def ensure_updated_shipments
|
||||
return unless shipments.any?
|
||||
|
||||
shipments.destroy_all
|
||||
update_column(:state, "address")
|
||||
end
|
||||
|
||||
def refresh_shipment_rates
|
||||
shipments.map(&:refresh_rates)
|
||||
end
|
||||
|
||||
# Check that line_items in the current order are available from a newly selected distribution
|
||||
def products_available_from_new_distribution
|
||||
return if OrderCycleDistributedVariants.new(order_cycle, distributor)
|
||||
.distributes_order_variants?(self)
|
||||
|
||||
errors.add(:base, I18n.t(:spree_order_availability_error))
|
||||
end
|
||||
|
||||
def disallow_guest_order
|
||||
return unless using_guest_checkout? && registered_email?
|
||||
|
||||
errors.add(:base, I18n.t('devise.failure.already_registered'))
|
||||
end
|
||||
|
||||
# After changing line items of a completed order
|
||||
def update_shipping_fees!
|
||||
shipments.each do |shipment|
|
||||
next if shipment.shipped?
|
||||
|
||||
update_adjustment! shipment.adjustment if shipment.adjustment
|
||||
save_or_rescue_shipment(shipment)
|
||||
end
|
||||
end
|
||||
|
||||
def save_or_rescue_shipment(shipment)
|
||||
shipment.save # updates included tax
|
||||
rescue ActiveRecord::RecordNotUnique => e
|
||||
# This error was seen in production on `shipment.save` above.
|
||||
# It caused lost payments and duplicate payments due to database rollbacks.
|
||||
# While we don't understand the cause of this error yet, we rescue here
|
||||
# because an outdated shipping fee is not as bad as a lost payment.
|
||||
# And the shipping fee is already up-to-date when this error occurs.
|
||||
# https://github.com/openfoodfoundation/openfoodnetwork/issues/3924
|
||||
Bugsnag.notify(e) do |report|
|
||||
report.add_tab(:order, attributes)
|
||||
report.add_tab(:shipment, shipment.attributes)
|
||||
report.add_tab(:shipment_in_db, Spree::Shipment.find_by(id: shipment.id).attributes)
|
||||
end
|
||||
end
|
||||
|
||||
# After changing line items of a completed order
|
||||
def update_payment_fees!
|
||||
payments.each do |payment|
|
||||
next if payment.completed?
|
||||
|
||||
update_adjustment! payment.adjustment if payment.adjustment
|
||||
payment.save
|
||||
end
|
||||
end
|
||||
|
||||
def update_distribution_charge!
|
||||
# `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
|
||||
with_lock do
|
||||
EnterpriseFee.clear_all_adjustments_on_order self
|
||||
|
||||
loaded_line_items =
|
||||
line_items.includes(variant: :product, order: [:distributor, :order_cycle]).all
|
||||
|
||||
loaded_line_items.each do |line_item|
|
||||
if provided_by_order_cycle? line_item
|
||||
OpenFoodNetwork::EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item
|
||||
end
|
||||
end
|
||||
|
||||
if order_cycle
|
||||
OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_order_cycle!(order_cycle)
|
||||
return if self.order_cycle == order_cycle
|
||||
|
||||
self.order_cycle = order_cycle
|
||||
self.distributor = nil unless order_cycle.nil? || order_cycle.has_distributor?(distributor)
|
||||
empty!
|
||||
save!
|
||||
end
|
||||
|
||||
def remove_variant(variant)
|
||||
line_items(:reload)
|
||||
current_item = find_line_item_by_variant(variant)
|
||||
current_item.andand.destroy
|
||||
end
|
||||
|
||||
def cap_quantity_at_stock!
|
||||
line_items.includes(variant: :stock_items).find_each(&:cap_quantity_at_stock!)
|
||||
end
|
||||
|
||||
def set_distributor!(distributor)
|
||||
self.distributor = distributor
|
||||
self.order_cycle = nil unless order_cycle.andand.has_distributor? distributor
|
||||
save!
|
||||
end
|
||||
|
||||
def set_distribution!(distributor, order_cycle)
|
||||
self.distributor = distributor
|
||||
self.order_cycle = order_cycle
|
||||
save!
|
||||
end
|
||||
|
||||
def distribution_set?
|
||||
distributor && order_cycle
|
||||
end
|
||||
|
||||
def shipping_tax
|
||||
adjustments(:reload).shipping.sum(&:included_tax)
|
||||
end
|
||||
|
||||
def enterprise_fee_tax
|
||||
adjustments(:reload).enterprise_fee.sum(&:included_tax)
|
||||
end
|
||||
|
||||
def total_tax
|
||||
(adjustments + price_adjustments).sum(&:included_tax)
|
||||
end
|
||||
|
||||
def price_adjustments
|
||||
adjustments = []
|
||||
|
||||
line_items.each { |line_item| adjustments.concat line_item.adjustments }
|
||||
|
||||
adjustments
|
||||
end
|
||||
|
||||
def price_adjustment_totals
|
||||
Hash[tax_adjustment_totals.map do |tax_rate, tax_amount|
|
||||
[tax_rate.name,
|
||||
Spree::Money.new(tax_amount, currency: currency)]
|
||||
end]
|
||||
end
|
||||
|
||||
def has_taxes_included
|
||||
!line_items.with_tax.empty?
|
||||
end
|
||||
|
||||
def address_from_distributor
|
||||
address = distributor.address.clone
|
||||
if bill_address
|
||||
address.firstname = bill_address.firstname
|
||||
address.lastname = bill_address.lastname
|
||||
address.phone = bill_address.phone
|
||||
end
|
||||
address
|
||||
end
|
||||
|
||||
# Update attributes of a record in the database without callbacks, validations etc.
|
||||
# This was originally an extension to ActiveRecord in Spree but only used for Spree::Order
|
||||
def update_attributes_without_callbacks(attributes)
|
||||
assign_attributes(attributes)
|
||||
Spree::Order.where(id: id).update_all(attributes)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_by_email
|
||||
self.email = user.email if user
|
||||
end
|
||||
|
||||
# Determine if email is required (we don't want validation errors before we hit the checkout)
|
||||
def require_email
|
||||
return true unless new_record? || (state == 'cart')
|
||||
end
|
||||
|
||||
def ensure_line_items_present
|
||||
return if line_items.present?
|
||||
|
||||
errors.add(:base, Spree.t(:there_are_no_items_for_this_order)) && (return false)
|
||||
end
|
||||
|
||||
def has_available_shipment
|
||||
return unless has_step?("delivery")
|
||||
return unless address?
|
||||
return unless ship_address&.valid?
|
||||
# errors.add(:base, :no_shipping_methods_available) if available_shipping_methods.empty?
|
||||
end
|
||||
|
||||
def ensure_available_shipping_rates
|
||||
return unless shipments.empty? || shipments.any? { |shipment| shipment.shipping_rates.blank? }
|
||||
|
||||
errors.add(:base, Spree.t(:items_cannot_be_shipped)) && (return false)
|
||||
end
|
||||
|
||||
def has_available_payment
|
||||
return unless delivery?
|
||||
# errors.add(:base, :no_payment_methods_available) if available_payment_methods.empty?
|
||||
end
|
||||
|
||||
def after_cancel
|
||||
shipments.each(&:cancel!)
|
||||
|
||||
OrderMailer.cancel_email(id).deliver
|
||||
self.payment_state = 'credit_owed' unless shipped?
|
||||
end
|
||||
|
||||
def after_resume
|
||||
shipments.each(&:resume!)
|
||||
end
|
||||
|
||||
def use_billing?
|
||||
@use_billing == true || @use_billing == 'true' || @use_billing == '1'
|
||||
end
|
||||
|
||||
def set_currency
|
||||
self.currency = Spree::Config[:currency] if self[:currency].nil?
|
||||
end
|
||||
|
||||
def using_guest_checkout?
|
||||
require_email && !user.andand.id
|
||||
end
|
||||
|
||||
def registered_email?
|
||||
Spree.user_class.exists?(email: email)
|
||||
end
|
||||
|
||||
def adjustments_fetcher
|
||||
@adjustments_fetcher ||= OrderAdjustmentsFetcher.new(self)
|
||||
end
|
||||
|
||||
def skip_payment_for_subscription?
|
||||
subscription.present? && order_cycle.orders_close_at.andand > Time.zone.now
|
||||
end
|
||||
|
||||
def provided_by_order_cycle?(line_item)
|
||||
order_cycle_variants = order_cycle.andand.variants || []
|
||||
order_cycle_variants.include? line_item.variant
|
||||
end
|
||||
|
||||
def require_customer?
|
||||
return true unless new_record? || state == 'cart'
|
||||
end
|
||||
|
||||
def customer_is_valid?
|
||||
return true unless require_customer?
|
||||
|
||||
customer.present? && customer.enterprise_id == distributor_id && customer.email == email_for_customer
|
||||
end
|
||||
|
||||
def email_for_customer
|
||||
(user.andand.email || email).andand.downcase
|
||||
end
|
||||
|
||||
def associate_customer
|
||||
return customer if customer.present?
|
||||
|
||||
self.customer = Customer.of(distributor).find_by(email: email_for_customer)
|
||||
end
|
||||
|
||||
def ensure_customer
|
||||
return if associate_customer
|
||||
|
||||
customer_name = bill_address.andand.full_name
|
||||
self.customer = Customer.create(enterprise: distributor, email: email_for_customer,
|
||||
user: user, name: customer_name,
|
||||
bill_address: bill_address.andand.clone,
|
||||
ship_address: ship_address.andand.clone)
|
||||
end
|
||||
|
||||
def update_adjustment!(adjustment)
|
||||
return if adjustment.finalized?
|
||||
|
||||
state = adjustment.state
|
||||
adjustment.state = 'open'
|
||||
adjustment.update!
|
||||
update!
|
||||
adjustment.state = state
|
||||
end
|
||||
|
||||
# object_params sets the payment amount to the order total, but it does this
|
||||
# before the shipping method is set. This results in the customer not being
|
||||
# charged for their order's shipping. To fix this, we refresh the payment
|
||||
# amount here.
|
||||
def charge_shipping_and_payment_fees!
|
||||
update_totals
|
||||
return unless pending_payments.any?
|
||||
|
||||
pending_payments.first.update_attribute :amount, total
|
||||
end
|
||||
end
|
||||
end
|
||||
68
app/models/spree/order_contents.rb
Normal file
68
app/models/spree/order_contents.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class OrderContents
|
||||
attr_accessor :order, :currency
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
# Get current line item for variant if exists
|
||||
# Add variant qty to line_item
|
||||
def add(variant, quantity = 1, currency = nil, shipment = nil)
|
||||
line_item = order.find_line_item_by_variant(variant)
|
||||
add_to_line_item(line_item, variant, quantity, currency, shipment)
|
||||
end
|
||||
|
||||
# Get current line item for variant
|
||||
# Remove variant qty from line_item
|
||||
def remove(variant, quantity = 1, shipment = nil)
|
||||
line_item = order.find_line_item_by_variant(variant)
|
||||
|
||||
unless line_item
|
||||
raise ActiveRecord::RecordNotFound, "Line item not found for variant #{variant.sku}"
|
||||
end
|
||||
|
||||
remove_from_line_item(line_item, variant, quantity, shipment)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_to_line_item(line_item, variant, quantity, currency = nil, shipment = nil)
|
||||
if line_item
|
||||
line_item.target_shipment = shipment
|
||||
line_item.quantity += quantity.to_i
|
||||
line_item.currency = currency unless currency.nil?
|
||||
else
|
||||
line_item = order.line_items.new(quantity: quantity, variant: variant)
|
||||
line_item.target_shipment = shipment
|
||||
if currency
|
||||
line_item.currency = currency unless currency.nil?
|
||||
line_item.price = variant.price_in(currency).amount
|
||||
else
|
||||
line_item.price = variant.price
|
||||
end
|
||||
end
|
||||
|
||||
line_item.save
|
||||
order.reload
|
||||
line_item
|
||||
end
|
||||
|
||||
def remove_from_line_item(line_item, _variant, quantity, shipment = nil)
|
||||
line_item.quantity += -quantity
|
||||
line_item.target_shipment = shipment
|
||||
|
||||
if line_item.quantity == 0
|
||||
Spree::OrderInventory.new(order).verify(line_item, shipment)
|
||||
line_item.destroy
|
||||
else
|
||||
line_item.save!
|
||||
end
|
||||
|
||||
order.reload
|
||||
line_item
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,446 +0,0 @@
|
||||
require 'open_food_network/enterprise_fee_calculator'
|
||||
require 'open_food_network/feature_toggle'
|
||||
require 'open_food_network/tag_rule_applicator'
|
||||
require 'concerns/order_shipment'
|
||||
|
||||
ActiveSupport::Notifications.subscribe('spree.order.contents_changed') do |_name, _start, _finish, _id, payload|
|
||||
payload[:order].reload.update_distribution_charge!
|
||||
end
|
||||
|
||||
Spree::Order.class_eval do
|
||||
prepend OrderShipment
|
||||
|
||||
delegate :admin_and_handling_total, :payment_fee, :ship_total, to: :adjustments_fetcher
|
||||
|
||||
belongs_to :order_cycle
|
||||
belongs_to :distributor, class_name: 'Enterprise'
|
||||
belongs_to :customer
|
||||
has_one :proxy_order
|
||||
has_one :subscription, through: :proxy_order
|
||||
|
||||
# This removes "inverse_of: source" which breaks shipment adjustment calculations
|
||||
# This change is done in Spree 2.1 (see https://github.com/spree/spree/commit/3fa44165c7825f79a2fa4eb79b99dc29944c5d55)
|
||||
# When OFN gets to Spree 2.1, this can be removed
|
||||
has_many :adjustments, -> { order "#{Spree::Adjustment.table_name}.created_at ASC" },
|
||||
as: :adjustable,
|
||||
dependent: :destroy
|
||||
|
||||
validates :customer, presence: true, if: :require_customer?
|
||||
validate :products_available_from_new_distribution, if: lambda { distributor_id_changed? || order_cycle_id_changed? }
|
||||
validate :disallow_guest_order
|
||||
|
||||
# The EmailValidator introduced in Spree 2.1 is not working
|
||||
# So here we remove it and re-introduce the regexp validation rule from Spree 2.0
|
||||
_validate_callbacks.each do |callback|
|
||||
if callback.raw_filter.respond_to? :attributes
|
||||
callback.raw_filter.attributes.delete :email
|
||||
end
|
||||
end
|
||||
validates :email, presence: true, format: /\A([\w\.%\+\-']+)@([\w\-]+\.)+([\w]{2,})\z/i,
|
||||
if: :require_email
|
||||
|
||||
before_validation :associate_customer, unless: :customer_id?
|
||||
before_validation :ensure_customer, unless: :customer_is_valid?
|
||||
|
||||
before_save :update_shipping_fees!, if: :complete?
|
||||
before_save :update_payment_fees!, if: :complete?
|
||||
|
||||
# Orders are confirmed with their payment, we don't use the confirm step.
|
||||
# Here we remove that step from Spree's checkout state machine.
|
||||
# See: https://guides.spreecommerce.org/developer/checkout.html#modifying-the-checkout-flow
|
||||
remove_checkout_step :confirm
|
||||
|
||||
state_machine.after_transition to: :payment, do: :charge_shipping_and_payment_fees!
|
||||
|
||||
state_machine.event :restart_checkout do
|
||||
transition to: :cart, unless: :completed?
|
||||
end
|
||||
|
||||
# -- Scopes
|
||||
scope :managed_by, lambda { |user|
|
||||
if user.has_spree_role?('admin')
|
||||
where(nil)
|
||||
else
|
||||
# Find orders that are distributed by the user or have products supplied by the user
|
||||
# WARNING: This only filters orders, you'll need to filter line items separately using LineItem.managed_by
|
||||
with_line_items_variants_and_products_outer.
|
||||
where('spree_orders.distributor_id IN (?) OR spree_products.supplier_id IN (?)',
|
||||
user.enterprises.select(&:id),
|
||||
user.enterprises.select(&:id)).
|
||||
select('DISTINCT spree_orders.*')
|
||||
end
|
||||
}
|
||||
|
||||
scope :distributed_by_user, lambda { |user|
|
||||
if user.has_spree_role?('admin')
|
||||
where(nil)
|
||||
else
|
||||
where('spree_orders.distributor_id IN (?)', user.enterprises.select(&:id))
|
||||
end
|
||||
}
|
||||
|
||||
scope :with_line_items_variants_and_products_outer, lambda {
|
||||
joins('LEFT OUTER JOIN spree_line_items ON (spree_line_items.order_id = spree_orders.id)').
|
||||
joins('LEFT OUTER JOIN spree_variants ON (spree_variants.id = spree_line_items.variant_id)').
|
||||
joins('LEFT OUTER JOIN spree_products ON (spree_products.id = spree_variants.product_id)')
|
||||
}
|
||||
|
||||
scope :not_state, lambda { |state|
|
||||
where("state != ?", state)
|
||||
}
|
||||
|
||||
def updater
|
||||
@updater ||= OrderManagement::Order::Updater.new(self)
|
||||
end
|
||||
|
||||
def create_proposed_shipments
|
||||
adjustments.shipping.delete_all
|
||||
shipments.destroy_all
|
||||
|
||||
packages = OrderManagement::Stock::Coordinator.new(self).packages
|
||||
packages.each do |package|
|
||||
shipments << package.to_shipment
|
||||
end
|
||||
|
||||
shipments
|
||||
end
|
||||
|
||||
# -- Methods
|
||||
def products_available_from_new_distribution
|
||||
# Check that the line_items in the current order are available from a newly selected distribution
|
||||
errors.add(:base, I18n.t(:spree_order_availability_error)) unless OrderCycleDistributedVariants.new(order_cycle, distributor).distributes_order_variants?(self)
|
||||
end
|
||||
|
||||
def using_guest_checkout?
|
||||
require_email && !user.andand.id
|
||||
end
|
||||
|
||||
def registered_email?
|
||||
Spree.user_class.exists?(email: email)
|
||||
end
|
||||
|
||||
def disallow_guest_order
|
||||
if using_guest_checkout? && registered_email?
|
||||
errors.add(:base, I18n.t('devise.failure.already_registered'))
|
||||
end
|
||||
end
|
||||
|
||||
def empty_with_clear_shipping_and_payments!
|
||||
empty_without_clear_shipping_and_payments!
|
||||
payments.clear
|
||||
shipments.destroy_all
|
||||
end
|
||||
alias_method_chain :empty!, :clear_shipping_and_payments
|
||||
|
||||
def set_order_cycle!(order_cycle)
|
||||
return if self.order_cycle == order_cycle
|
||||
|
||||
self.order_cycle = order_cycle
|
||||
self.distributor = nil unless order_cycle.nil? || order_cycle.has_distributor?(distributor)
|
||||
empty!
|
||||
save!
|
||||
end
|
||||
|
||||
# "Checkout" is the initial state and, for card payments, "pending" is the state after authorization
|
||||
# These are both valid states to process the payment
|
||||
def pending_payments
|
||||
(payments.select(&:pending?) + payments.select(&:processing?) + payments.select(&:checkout?)).uniq
|
||||
end
|
||||
|
||||
def remove_variant(variant)
|
||||
line_items(:reload)
|
||||
current_item = find_line_item_by_variant(variant)
|
||||
current_item.andand.destroy
|
||||
end
|
||||
|
||||
# Overridden to support max_quantity
|
||||
def add_variant(variant, quantity = 1, max_quantity = nil, currency = nil)
|
||||
line_items(:reload)
|
||||
current_item = find_line_item_by_variant(variant)
|
||||
|
||||
# Notify bugsnag if we get line items with a quantity of zero
|
||||
if quantity == 0
|
||||
Bugsnag.notify(RuntimeError.new("Zero Quantity Line Item"),
|
||||
current_item: current_item.as_json,
|
||||
line_items: line_items.map(&:id),
|
||||
variant: variant.as_json)
|
||||
end
|
||||
|
||||
if current_item
|
||||
current_item.quantity = quantity
|
||||
current_item.max_quantity = max_quantity
|
||||
|
||||
# This is the original behaviour, behaviour above is so that we can resolve the order populator bug
|
||||
# current_item.quantity ||= 0
|
||||
# current_item.max_quantity ||= 0
|
||||
# current_item.quantity += quantity.to_i
|
||||
# current_item.max_quantity += max_quantity.to_i
|
||||
current_item.currency = currency unless currency.nil?
|
||||
current_item.save
|
||||
else
|
||||
current_item = Spree::LineItem.new(quantity: quantity, max_quantity: max_quantity)
|
||||
current_item.variant = variant
|
||||
if currency
|
||||
current_item.currency = currency unless currency.nil?
|
||||
current_item.price = variant.price_in(currency).amount
|
||||
else
|
||||
current_item.price = variant.price
|
||||
end
|
||||
line_items << current_item
|
||||
end
|
||||
|
||||
reload
|
||||
current_item
|
||||
end
|
||||
|
||||
# After changing line items of a completed order
|
||||
def update_shipping_fees!
|
||||
shipments.each do |shipment|
|
||||
next if shipment.shipped?
|
||||
|
||||
update_adjustment! shipment.adjustment if shipment.adjustment
|
||||
save_or_rescue_shipment(shipment)
|
||||
end
|
||||
end
|
||||
|
||||
def save_or_rescue_shipment(shipment)
|
||||
shipment.save # updates included tax
|
||||
rescue ActiveRecord::RecordNotUnique => e
|
||||
# This error was seen in production on `shipment.save` above.
|
||||
# It caused lost payments and duplicate payments due to database rollbacks.
|
||||
# While we don't understand the cause of this error yet, we rescue here
|
||||
# because an outdated shipping fee is not as bad as a lost payment.
|
||||
# And the shipping fee is already up-to-date when this error occurs.
|
||||
# https://github.com/openfoodfoundation/openfoodnetwork/issues/3924
|
||||
Bugsnag.notify(e) do |report|
|
||||
report.add_tab(:order, attributes)
|
||||
report.add_tab(:shipment, shipment.attributes)
|
||||
report.add_tab(:shipment_in_db, Spree::Shipment.find_by(id: shipment.id).attributes)
|
||||
end
|
||||
end
|
||||
|
||||
# After changing line items of a completed order
|
||||
def update_payment_fees!
|
||||
payments.each do |payment|
|
||||
next if payment.completed?
|
||||
|
||||
update_adjustment! payment.adjustment if payment.adjustment
|
||||
payment.save
|
||||
end
|
||||
end
|
||||
|
||||
def cap_quantity_at_stock!
|
||||
line_items.includes(variant: :stock_items).all.each(&:cap_quantity_at_stock!)
|
||||
end
|
||||
|
||||
def set_distributor!(distributor)
|
||||
self.distributor = distributor
|
||||
self.order_cycle = nil unless order_cycle.andand.has_distributor? distributor
|
||||
save!
|
||||
end
|
||||
|
||||
def set_distribution!(distributor, order_cycle)
|
||||
self.distributor = distributor
|
||||
self.order_cycle = order_cycle
|
||||
save!
|
||||
end
|
||||
|
||||
def distribution_set?
|
||||
distributor && order_cycle
|
||||
end
|
||||
|
||||
def update_distribution_charge!
|
||||
# `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
|
||||
with_lock do
|
||||
EnterpriseFee.clear_all_adjustments_on_order self
|
||||
|
||||
loaded_line_items =
|
||||
line_items.includes(variant: :product, order: [:distributor, :order_cycle]).all
|
||||
|
||||
loaded_line_items.each do |line_item|
|
||||
if provided_by_order_cycle? line_item
|
||||
OpenFoodNetwork::EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item
|
||||
end
|
||||
end
|
||||
|
||||
if order_cycle
|
||||
OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_variant_attributes(variant, attributes)
|
||||
line_item = find_line_item_by_variant(variant)
|
||||
|
||||
if line_item
|
||||
if attributes.key?(:max_quantity) && attributes[:max_quantity].to_i < line_item.quantity
|
||||
attributes[:max_quantity] = line_item.quantity
|
||||
end
|
||||
|
||||
line_item.assign_attributes(attributes)
|
||||
line_item.save!
|
||||
end
|
||||
end
|
||||
|
||||
def line_item_variants
|
||||
if line_items.loaded?
|
||||
line_items.map(&:variant)
|
||||
else
|
||||
line_items.includes(:variant).map(&:variant)
|
||||
end
|
||||
end
|
||||
|
||||
# Show already bought line items of this order cycle
|
||||
def finalised_line_items
|
||||
return [] unless order_cycle && user && distributor
|
||||
|
||||
order_cycle.items_bought_by_user(user, distributor)
|
||||
end
|
||||
|
||||
# Does this order have shipments that can be shipped?
|
||||
def ready_to_ship?
|
||||
shipments.any?(&:can_ship?)
|
||||
end
|
||||
|
||||
# Ship all pending orders
|
||||
def ship
|
||||
shipments.each do |s|
|
||||
s.ship if s.can_ship?
|
||||
end
|
||||
end
|
||||
|
||||
def shipping_tax
|
||||
adjustments(:reload).shipping.sum(&:included_tax)
|
||||
end
|
||||
|
||||
def enterprise_fee_tax
|
||||
adjustments(:reload).enterprise_fee.sum(&:included_tax)
|
||||
end
|
||||
|
||||
def total_tax
|
||||
(adjustments + price_adjustments).sum(&:included_tax)
|
||||
end
|
||||
|
||||
def price_adjustments
|
||||
adjustments = []
|
||||
|
||||
line_items.each { |line_item| adjustments.concat line_item.adjustments }
|
||||
|
||||
adjustments
|
||||
end
|
||||
|
||||
def price_adjustment_totals
|
||||
Hash[tax_adjustment_totals.map do |tax_rate, tax_amount|
|
||||
[tax_rate.name,
|
||||
Spree::Money.new(tax_amount, currency: currency)]
|
||||
end]
|
||||
end
|
||||
|
||||
def has_taxes_included
|
||||
!line_items.with_tax.empty?
|
||||
end
|
||||
|
||||
# Overrride of Spree method, that allows us to send separate confirmation emails to user and shop owners
|
||||
def deliver_order_confirmation_email
|
||||
if subscription.blank?
|
||||
Delayed::Job.enqueue ConfirmOrderJob.new(id)
|
||||
end
|
||||
end
|
||||
|
||||
def changes_allowed?
|
||||
complete? && distributor.andand.allow_order_changes? && order_cycle.andand.open?
|
||||
end
|
||||
|
||||
# Override Spree method to allow unpaid orders to be completed.
|
||||
# Subscriptions place orders at the beginning of an order cycle. They need to
|
||||
# be completed to draw from stock levels and trigger emails.
|
||||
# Spree doesn't allow this. Other options would be to introduce an additional
|
||||
# order state or implement a special proxy payment method.
|
||||
# https://github.com/openfoodfoundation/openfoodnetwork/pull/3012#issuecomment-438146484
|
||||
def payment_required?
|
||||
total.to_f > 0.0 && !skip_payment_for_subscription?
|
||||
end
|
||||
|
||||
def address_from_distributor
|
||||
address = distributor.address.clone
|
||||
if bill_address
|
||||
address.firstname = bill_address.firstname
|
||||
address.lastname = bill_address.lastname
|
||||
address.phone = bill_address.phone
|
||||
end
|
||||
address
|
||||
end
|
||||
|
||||
# Update attributes of a record in the database without callbacks, validations etc.
|
||||
# This was originally an extension to ActiveRecord in Spree but only used for Spree::Order
|
||||
def update_attributes_without_callbacks(attributes)
|
||||
assign_attributes(attributes)
|
||||
Spree::Order.where(id: id).update_all(attributes)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def adjustments_fetcher
|
||||
@adjustments_fetcher ||= OrderAdjustmentsFetcher.new(self)
|
||||
end
|
||||
|
||||
def skip_payment_for_subscription?
|
||||
subscription.present? && order_cycle.orders_close_at.andand > Time.zone.now
|
||||
end
|
||||
|
||||
def provided_by_order_cycle?(line_item)
|
||||
order_cycle_variants = order_cycle.andand.variants || []
|
||||
order_cycle_variants.include? line_item.variant
|
||||
end
|
||||
|
||||
def require_customer?
|
||||
return true unless new_record? || state == 'cart'
|
||||
end
|
||||
|
||||
def customer_is_valid?
|
||||
return true unless require_customer?
|
||||
|
||||
customer.present? && customer.enterprise_id == distributor_id && customer.email == email_for_customer
|
||||
end
|
||||
|
||||
def email_for_customer
|
||||
(user.andand.email || email).andand.downcase
|
||||
end
|
||||
|
||||
def associate_customer
|
||||
return customer if customer.present?
|
||||
|
||||
self.customer = Customer.of(distributor).find_by(email: email_for_customer)
|
||||
end
|
||||
|
||||
def ensure_customer
|
||||
unless associate_customer
|
||||
customer_name = bill_address.andand.full_name
|
||||
self.customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user, name: customer_name, bill_address: bill_address.andand.clone, ship_address: ship_address.andand.clone)
|
||||
end
|
||||
end
|
||||
|
||||
def update_adjustment!(adjustment)
|
||||
return if adjustment.finalized?
|
||||
|
||||
state = adjustment.state
|
||||
adjustment.state = 'open'
|
||||
adjustment.update!
|
||||
update!
|
||||
adjustment.state = state
|
||||
end
|
||||
|
||||
# object_params sets the payment amount to the order total, but it does this
|
||||
# before the shipping method is set. This results in the customer not being
|
||||
# charged for their order's shipping. To fix this, we refresh the payment
|
||||
# amount here.
|
||||
def charge_shipping_and_payment_fees!
|
||||
update_totals
|
||||
return unless pending_payments.any?
|
||||
|
||||
pending_payments.first.update_attribute :amount, total
|
||||
end
|
||||
end
|
||||
112
app/models/spree/order_inventory.rb
Normal file
112
app/models/spree/order_inventory.rb
Normal file
@@ -0,0 +1,112 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class OrderInventory
|
||||
attr_accessor :order
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
# Only verify inventory for completed orders (as orders in frontend checkout
|
||||
# have inventory assigned via +order.create_proposed_shipment+) or when
|
||||
# shipment is explicitly passed
|
||||
#
|
||||
# In case shipment is passed the stock location should only unstock or
|
||||
# restock items if the order is completed. That is so because stock items
|
||||
# are always unstocked when the order is completed through +shipment.finalize+
|
||||
def verify(line_item, shipment = nil)
|
||||
if order.completed? || shipment.present?
|
||||
|
||||
variant_units = inventory_units_for(line_item.variant)
|
||||
|
||||
if variant_units.size < line_item.quantity
|
||||
quantity = line_item.quantity - variant_units.size
|
||||
|
||||
shipment ||= determine_target_shipment(line_item.variant)
|
||||
add_to_shipment(shipment, line_item.variant, quantity)
|
||||
elsif variant_units.size > line_item.quantity
|
||||
remove(line_item, variant_units, shipment)
|
||||
end
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def inventory_units_for(variant)
|
||||
units = order.shipments.collect{ |s| s.inventory_units.to_a }.flatten
|
||||
units.group_by(&:variant_id)[variant.id] || []
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove(line_item, variant_units, shipment = nil)
|
||||
quantity = variant_units.size - line_item.quantity
|
||||
|
||||
if shipment.present?
|
||||
remove_from_shipment(shipment, line_item.variant, quantity)
|
||||
else
|
||||
order.shipments.each do |each_shipment|
|
||||
break if quantity == 0
|
||||
|
||||
quantity -= remove_from_shipment(each_shipment, line_item.variant, quantity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns either one of the shipment:
|
||||
#
|
||||
# first unshipped that already includes this variant
|
||||
# first unshipped that's leaving from a stock_location that stocks this variant
|
||||
def determine_target_shipment(variant)
|
||||
target_shipment = order.shipments.detect do |shipment|
|
||||
(shipment.ready? || shipment.pending?) && shipment.include?(variant)
|
||||
end
|
||||
|
||||
target_shipment || order.shipments.detect do |shipment|
|
||||
(shipment.ready? || shipment.pending?) &&
|
||||
variant.stock_location_ids.include?(shipment.stock_location_id)
|
||||
end
|
||||
end
|
||||
|
||||
def add_to_shipment(shipment, variant, quantity)
|
||||
on_hand, back_order = shipment.stock_location.fill_status(variant, quantity)
|
||||
|
||||
on_hand.times { shipment.set_up_inventory('on_hand', variant, order) }
|
||||
back_order.times { shipment.set_up_inventory('backordered', variant, order) }
|
||||
|
||||
# adding to this shipment, and removing from stock_location
|
||||
if order.completed?
|
||||
shipment.stock_location.unstock(variant, quantity, shipment)
|
||||
end
|
||||
|
||||
quantity
|
||||
end
|
||||
|
||||
def remove_from_shipment(shipment, variant, quantity)
|
||||
return 0 if quantity == 0 || shipment.shipped?
|
||||
|
||||
shipment_units = shipment.inventory_units_for(variant).reject do |variant_unit|
|
||||
variant_unit.state == 'shipped'
|
||||
end.sort_by(&:state)
|
||||
|
||||
removed_quantity = 0
|
||||
|
||||
shipment_units.each do |inventory_unit|
|
||||
break if removed_quantity == quantity
|
||||
|
||||
inventory_unit.destroy
|
||||
removed_quantity += 1
|
||||
end
|
||||
|
||||
shipment.destroy if shipment.inventory_units.count == 0
|
||||
|
||||
# removing this from shipment, and adding to stock_location
|
||||
if order.completed?
|
||||
shipment.stock_location.restock variant, removed_quantity, shipment
|
||||
end
|
||||
|
||||
removed_quantity
|
||||
end
|
||||
end
|
||||
end
|
||||
108
app/models/spree/return_authorization.rb
Normal file
108
app/models/spree/return_authorization.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class ReturnAuthorization < ActiveRecord::Base
|
||||
belongs_to :order, class_name: 'Spree::Order'
|
||||
|
||||
has_many :inventory_units
|
||||
has_one :stock_location
|
||||
before_create :generate_number
|
||||
before_save :force_positive_amount
|
||||
|
||||
validates :order, presence: true
|
||||
validates :amount, numericality: true
|
||||
validate :must_have_shipped_units
|
||||
|
||||
state_machine initial: :authorized do
|
||||
after_transition to: :received, do: :process_return
|
||||
|
||||
event :receive do
|
||||
transition to: :received, from: :authorized, if: :allow_receive?
|
||||
end
|
||||
event :cancel do
|
||||
transition to: :canceled, from: :authorized
|
||||
end
|
||||
end
|
||||
|
||||
def currency
|
||||
order.nil? ? Spree::Config[:currency] : order.currency
|
||||
end
|
||||
|
||||
def display_amount
|
||||
Spree::Money.new(amount, currency: currency)
|
||||
end
|
||||
|
||||
def add_variant(variant_id, quantity)
|
||||
order_units = returnable_inventory.group_by(&:variant_id)
|
||||
returned_units = inventory_units.group_by(&:variant_id)
|
||||
return false if order_units.empty?
|
||||
|
||||
count = 0
|
||||
|
||||
if returned_units[variant_id].nil? || returned_units[variant_id].size < quantity
|
||||
count = returned_units[variant_id].nil? ? 0 : returned_units[variant_id].size
|
||||
|
||||
order_units[variant_id].each do |inventory_unit|
|
||||
next unless inventory_unit.return_authorization.nil? && count < quantity
|
||||
|
||||
inventory_unit.return_authorization = self
|
||||
inventory_unit.save!
|
||||
|
||||
count += 1
|
||||
end
|
||||
elsif returned_units[variant_id].size > quantity
|
||||
(returned_units[variant_id].size - quantity).times do |i|
|
||||
returned_units[variant_id][i].return_authorization_id = nil
|
||||
returned_units[variant_id][i].save!
|
||||
end
|
||||
end
|
||||
|
||||
order.authorize_return! if !inventory_units.reload.empty? && !order.awaiting_return?
|
||||
end
|
||||
|
||||
def returnable_inventory
|
||||
order.shipped_shipments.collect{ |s| s.inventory_units.to_a }.flatten
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def must_have_shipped_units
|
||||
return unless order.nil? || order.shipped_shipments.none?
|
||||
|
||||
errors.add(:order, Spree.t(:has_no_shipped_units))
|
||||
end
|
||||
|
||||
def generate_number
|
||||
return if number
|
||||
|
||||
record = true
|
||||
while record
|
||||
random = "RMA#{Array.new(9){ rand(9) }.join}"
|
||||
record = self.class.find_by(number: random)
|
||||
end
|
||||
self.number = random
|
||||
end
|
||||
|
||||
def process_return
|
||||
inventory_units.each do |iu|
|
||||
iu.return!
|
||||
Spree::StockMovement.create!(stock_item_id: iu.find_stock_item.id, quantity: 1)
|
||||
end
|
||||
|
||||
credit = Adjustment.new(amount: amount.abs * -1, label: Spree.t(:rma_credit))
|
||||
credit.source = self
|
||||
credit.adjustable = order
|
||||
credit.save
|
||||
|
||||
order.return if inventory_units.all?(&:returned?)
|
||||
end
|
||||
|
||||
def allow_receive?
|
||||
!inventory_units.empty?
|
||||
end
|
||||
|
||||
def force_positive_amount
|
||||
self.amount = amount.abs
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/models/spree/state_change.rb
Normal file
17
app/models/spree/state_change.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class StateChange < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :stateful, polymorphic: true
|
||||
before_create :assign_user
|
||||
|
||||
def <=>(other)
|
||||
created_at <=> other.created_at
|
||||
end
|
||||
|
||||
def assign_user
|
||||
true # don't stop the filters
|
||||
end
|
||||
end
|
||||
end
|
||||
7
app/models/spree/tokenized_permission.rb
Normal file
7
app/models/spree/tokenized_permission.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class TokenizedPermission < ActiveRecord::Base
|
||||
belongs_to :permissable, polymorphic: true
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'spree/core/gateway_error'
|
||||
|
||||
describe Spree::Admin::PaymentsController, type: :controller do
|
||||
include StripeHelper
|
||||
|
||||
75
spec/models/spree/inventory_unit_spec.rb
Normal file
75
spec/models/spree/inventory_unit_spec.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::InventoryUnit do
|
||||
let(:stock_location) { create(:stock_location_with_items) }
|
||||
let(:stock_item) { stock_location.stock_items.order(:id).first }
|
||||
|
||||
context "#backordered_for_stock_item" do
|
||||
let(:order) { create(:order) }
|
||||
|
||||
let(:shipment) do
|
||||
shipment = Spree::Shipment.new
|
||||
shipment.stock_location = stock_location
|
||||
shipment.shipping_methods << create(:shipping_method)
|
||||
shipment.order = order
|
||||
# We don't care about this in this test
|
||||
allow(shipment).to receive(:ensure_correct_adjustment)
|
||||
shipment.tap(&:save!)
|
||||
end
|
||||
|
||||
let!(:unit) do
|
||||
unit = shipment.inventory_units.build
|
||||
unit.state = 'backordered'
|
||||
unit.variant_id = stock_item.variant.id
|
||||
unit.tap(&:save!)
|
||||
end
|
||||
|
||||
# Regression for Spree #3066
|
||||
it "returns modifiable objects" do
|
||||
units = Spree::InventoryUnit.backordered_for_stock_item(stock_item)
|
||||
expect { units.first.save! }.to_not raise_error
|
||||
end
|
||||
|
||||
it "finds inventory units from its stock location when the unit's variant matches the stock item's variant" do
|
||||
expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).to eq [unit]
|
||||
end
|
||||
|
||||
it "does not find inventory units that don't match the stock item's variant" do
|
||||
other_variant_unit = shipment.inventory_units.build
|
||||
other_variant_unit.state = 'backordered'
|
||||
other_variant_unit.variant = create(:variant)
|
||||
other_variant_unit.save!
|
||||
|
||||
expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).to_not include(other_variant_unit)
|
||||
end
|
||||
end
|
||||
|
||||
context "variants deleted" do
|
||||
let!(:unit) do
|
||||
Spree::InventoryUnit.create(variant: stock_item.variant)
|
||||
end
|
||||
|
||||
it "can still fetch variant" do
|
||||
unit.variant.destroy
|
||||
expect(unit.reload.variant).to be_a Spree::Variant
|
||||
end
|
||||
end
|
||||
|
||||
context "#finalize_units!" do
|
||||
let!(:stock_location) { create(:stock_location) }
|
||||
let(:variant) { create(:variant) }
|
||||
let(:inventory_units) {
|
||||
[
|
||||
create(:inventory_unit, variant: variant),
|
||||
create(:inventory_unit, variant: variant)
|
||||
]
|
||||
}
|
||||
|
||||
it "should create a stock movement" do
|
||||
Spree::InventoryUnit.finalize_units!(inventory_units)
|
||||
expect(inventory_units.any?(&:pending)).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,143 @@ require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe LineItem do
|
||||
let(:order) { create :order_with_line_items, line_items_count: 1 }
|
||||
let(:line_item) { order.line_items.first }
|
||||
|
||||
context '#save' do
|
||||
it 'should update inventory, totals, and tax' do
|
||||
# Regression check for Spree #1481
|
||||
expect(line_item.order).to receive(:create_tax_charge!)
|
||||
expect(line_item.order).to receive(:update!)
|
||||
line_item.quantity = 2
|
||||
line_item.save
|
||||
end
|
||||
end
|
||||
|
||||
context '#destroy' do
|
||||
# Regression test for Spree #1481
|
||||
it "applies tax adjustments" do
|
||||
expect(line_item.order).to receive(:create_tax_charge!)
|
||||
line_item.destroy
|
||||
end
|
||||
|
||||
it "fetches deleted products" do
|
||||
line_item.product.destroy
|
||||
expect(line_item.reload.product).to be_a Spree::Product
|
||||
end
|
||||
|
||||
it "fetches deleted variants" do
|
||||
line_item.variant.destroy
|
||||
expect(line_item.reload.variant).to be_a Spree::Variant
|
||||
end
|
||||
end
|
||||
|
||||
# Test for Spree #3391
|
||||
context '#copy_price' do
|
||||
it "copies over a variant's prices" do
|
||||
line_item.price = nil
|
||||
line_item.cost_price = nil
|
||||
line_item.currency = nil
|
||||
line_item.copy_price
|
||||
variant = line_item.variant
|
||||
expect(line_item.price).to eq variant.price
|
||||
expect(line_item.cost_price).to eq variant.cost_price
|
||||
expect(line_item.currency).to eq variant.currency
|
||||
end
|
||||
end
|
||||
|
||||
# Test for Spree #3481
|
||||
context '#copy_tax_category' do
|
||||
it "copies over a variant's tax category" do
|
||||
line_item.tax_category = nil
|
||||
line_item.copy_tax_category
|
||||
expect(line_item.tax_category).to eq line_item.variant.product.tax_category
|
||||
end
|
||||
end
|
||||
|
||||
describe '.currency' do
|
||||
it 'returns the globally configured currency' do
|
||||
line_item.currency == 'USD'
|
||||
end
|
||||
end
|
||||
|
||||
describe ".money" do
|
||||
before do
|
||||
line_item.price = 3.50
|
||||
line_item.quantity = 2
|
||||
end
|
||||
|
||||
it "returns a Spree::Money representing the total for this line item" do
|
||||
expect(line_item.money.to_s).to eq "$7.00"
|
||||
end
|
||||
end
|
||||
|
||||
describe '.single_money' do
|
||||
before { line_item.price = 3.50 }
|
||||
it "returns a Spree::Money representing the price for one variant" do
|
||||
expect(line_item.single_money.to_s).to eq "$3.50"
|
||||
end
|
||||
end
|
||||
|
||||
context "has inventory (completed order so items were already unstocked)" do
|
||||
let(:order) { Spree::Order.create }
|
||||
let(:variant) { create(:variant) }
|
||||
|
||||
context "nothing left on stock" do
|
||||
before do
|
||||
variant.stock_items.update_all count_on_hand: 5, backorderable: false
|
||||
order.contents.add(variant, 5)
|
||||
order.create_proposed_shipments
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
it "allows to decrease item quantity" do
|
||||
line_item = order.line_items.first
|
||||
line_item.quantity -= 1
|
||||
line_item.target_shipment = order.shipments.first
|
||||
|
||||
line_item.save
|
||||
expect(line_item.errors[:quantity]).to be_empty
|
||||
end
|
||||
|
||||
it "doesnt allow to increase item quantity" do
|
||||
line_item = order.line_items.first
|
||||
line_item.quantity += 2
|
||||
line_item.target_shipment = order.shipments.first
|
||||
|
||||
line_item.save
|
||||
expect(line_item.errors[:quantity].first).to include "is out of stock"
|
||||
end
|
||||
end
|
||||
|
||||
context "2 items left on stock" do
|
||||
before do
|
||||
variant.stock_items.update_all count_on_hand: 7, backorderable: false
|
||||
order.contents.add(variant, 5)
|
||||
order.create_proposed_shipments
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
it "allows to increase quantity up to stock availability" do
|
||||
line_item = order.line_items.first
|
||||
line_item.quantity += 2
|
||||
line_item.target_shipment = order.shipments.first
|
||||
|
||||
line_item.save
|
||||
expect(line_item.errors[:quantity]).to be_empty
|
||||
end
|
||||
|
||||
it "doesnt allow to increase quantity over stock availability" do
|
||||
line_item = order.line_items.first
|
||||
line_item.quantity += 3
|
||||
line_item.target_shipment = order.shipments.first
|
||||
|
||||
line_item.save
|
||||
expect(line_item.errors[:quantity].first).to include "is out of stock"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "scopes" do
|
||||
let(:o) { create(:order) }
|
||||
|
||||
|
||||
52
spec/models/spree/order/address_spec.rb
Normal file
52
spec/models/spree/order/address_spec.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Order do
|
||||
let(:order) { Spree::Order.new }
|
||||
|
||||
context 'validation' do
|
||||
context "when @use_billing is populated" do
|
||||
before do
|
||||
order.bill_address = build(:address)
|
||||
order.ship_address = nil
|
||||
end
|
||||
|
||||
context "with true" do
|
||||
before { order.use_billing = true }
|
||||
|
||||
it "clones the bill address to the ship address" do
|
||||
order.valid?
|
||||
expect(order.ship_address).to eq order.bill_address
|
||||
end
|
||||
end
|
||||
|
||||
context "with 'true'" do
|
||||
before { order.use_billing = 'true' }
|
||||
|
||||
it "clones the bill address to the shipping" do
|
||||
order.valid?
|
||||
expect(order.ship_address).to eq order.bill_address
|
||||
end
|
||||
end
|
||||
|
||||
context "with '1'" do
|
||||
before { order.use_billing = '1' }
|
||||
|
||||
it "clones the bill address to the shipping" do
|
||||
order.valid?
|
||||
expect(order.ship_address).to eq order.bill_address
|
||||
end
|
||||
end
|
||||
|
||||
context "with something other than a 'truthful' value" do
|
||||
before { order.use_billing = '0' }
|
||||
|
||||
it "does not clone the bill address to the shipping" do
|
||||
order.valid?
|
||||
expect(order.ship_address).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
147
spec/models/spree/order/adjustments_spec.rb
Normal file
147
spec/models/spree/order/adjustments_spec.rb
Normal file
@@ -0,0 +1,147 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
describe Spree::Order do
|
||||
let(:order) { Spree::Order.new }
|
||||
|
||||
context "clear_adjustments" do
|
||||
let(:adjustment) { double("Adjustment") }
|
||||
|
||||
it "destroys all order adjustments" do
|
||||
allow(order).to receive_messages(adjustments: adjustment)
|
||||
expect(adjustment).to receive(:destroy_all)
|
||||
order.clear_adjustments!
|
||||
end
|
||||
|
||||
it "destroy all line item adjustments" do
|
||||
allow(order).to receive_messages(line_item_adjustments: adjustment)
|
||||
expect(adjustment).to receive(:destroy_all)
|
||||
order.clear_adjustments!
|
||||
end
|
||||
end
|
||||
|
||||
context "totaling adjustments" do
|
||||
let(:adjustment1) { build(:adjustment, amount: 5) }
|
||||
let(:adjustment2) { build(:adjustment, amount: 10) }
|
||||
|
||||
context "#ship_total" do
|
||||
it "should return the correct amount" do
|
||||
allow(order).to receive_message_chain :adjustments, shipping: [adjustment1, adjustment2]
|
||||
expect(order.ship_total).to eq 15
|
||||
end
|
||||
end
|
||||
|
||||
context "#tax_total" do
|
||||
it "should return the correct amount" do
|
||||
allow(order).to receive_message_chain :adjustments, tax: [adjustment1, adjustment2]
|
||||
expect(order.tax_total).to eq 15
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "line item adjustment totals" do
|
||||
before { @order = Spree::Order.create! }
|
||||
|
||||
context "when there are no line item adjustments" do
|
||||
before { allow(@order).to receive_message_chain(:line_item_adjustments, eligible: []) }
|
||||
|
||||
it "should return an empty hash" do
|
||||
expect(@order.line_item_adjustment_totals).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are two adjustments with different labels" do
|
||||
let(:adj1) { build(:adjustment, amount: 10, label: "Foo") }
|
||||
let(:adj2) { build(:adjustment, amount: 20, label: "Bar") }
|
||||
|
||||
before do
|
||||
allow(@order).to receive_message_chain(:line_item_adjustments, eligible: [adj1, adj2])
|
||||
end
|
||||
|
||||
it "should return exactly two totals" do
|
||||
expect(@order.line_item_adjustment_totals.size).to eq 2
|
||||
end
|
||||
|
||||
it "should return the correct totals" do
|
||||
expect(@order.line_item_adjustment_totals["Foo"]).to eq Spree::Money.new(10)
|
||||
expect(@order.line_item_adjustment_totals["Bar"]).to eq Spree::Money.new(20)
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are two adjustments with one label and a single adjustment with another" do
|
||||
let(:adj1) { build(:adjustment, amount: 10, label: "Foo") }
|
||||
let(:adj2) { build(:adjustment, amount: 20, label: "Bar") }
|
||||
let(:adj3) { build(:adjustment, amount: 40, label: "Bar") }
|
||||
|
||||
before do
|
||||
allow(@order).to receive_message_chain(:line_item_adjustments, eligible: [adj1, adj2, adj3])
|
||||
end
|
||||
|
||||
it "should return exactly two totals" do
|
||||
expect(@order.line_item_adjustment_totals.size).to eq 2
|
||||
end
|
||||
it "should return the correct totals" do
|
||||
expect(@order.line_item_adjustment_totals["Foo"]).to eq Spree::Money.new(10)
|
||||
expect(@order.line_item_adjustment_totals["Bar"]).to eq Spree::Money.new(60)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "line item adjustments" do
|
||||
before do
|
||||
@order = Spree::Order.create!
|
||||
allow(@order).to receive_messages line_items: [line_item1, line_item2]
|
||||
end
|
||||
|
||||
let(:line_item1) { create(:line_item, order: @order) }
|
||||
let(:line_item2) { create(:line_item, order: @order) }
|
||||
|
||||
context "when there are no line item adjustments" do
|
||||
it "should return nothing if line items have no adjustments" do
|
||||
expect(@order.line_item_adjustments).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when only one line item has adjustments" do
|
||||
before do
|
||||
@adj1 = line_item1.adjustments.create(
|
||||
amount: 2,
|
||||
source: line_item1,
|
||||
label: "VAT 5%"
|
||||
)
|
||||
|
||||
@adj2 = line_item1.adjustments.create(
|
||||
amount: 5,
|
||||
source: line_item1,
|
||||
label: "VAT 10%"
|
||||
)
|
||||
end
|
||||
|
||||
it "should return the adjustments for that line item" do
|
||||
expect(@order.line_item_adjustments).to include @adj1
|
||||
expect(@order.line_item_adjustments).to include @adj2
|
||||
end
|
||||
end
|
||||
|
||||
context "when more than one line item has adjustments" do
|
||||
before do
|
||||
@adj1 = line_item1.adjustments.create(
|
||||
amount: 2,
|
||||
source: line_item1,
|
||||
label: "VAT 5%"
|
||||
)
|
||||
|
||||
@adj2 = line_item2.adjustments.create(
|
||||
amount: 5,
|
||||
source: line_item2,
|
||||
label: "VAT 10%"
|
||||
)
|
||||
end
|
||||
|
||||
it "should return the adjustments for each line item" do
|
||||
expect(@order.line_item_adjustments).to include @adj1
|
||||
expect(@order.line_item_adjustments).to include @adj2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
44
spec/models/spree/order/callbacks_spec.rb
Normal file
44
spec/models/spree/order/callbacks_spec.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Order do
|
||||
let(:order) { build(:order) }
|
||||
before do
|
||||
Spree::Order.define_state_machine!
|
||||
end
|
||||
|
||||
context "validations" do
|
||||
context "email validation" do
|
||||
# Regression test for Spree #1238
|
||||
it "o'brien@gmail.com is a valid email address" do
|
||||
order.state = 'address'
|
||||
order.email = "o'brien@gmail.com"
|
||||
expect(order.errors[:email]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#save" do
|
||||
context "when associated with a registered user" do
|
||||
let(:user) { double(:user, email: "test@example.com") }
|
||||
|
||||
before do
|
||||
allow(order).to receive_messages user: user
|
||||
end
|
||||
|
||||
it "should assign the email address of the user" do
|
||||
order.run_callbacks(:create)
|
||||
expect(order.email).to eq user.email
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "in the cart state" do
|
||||
it "should not validate email address" do
|
||||
order.state = "cart"
|
||||
order.email = nil
|
||||
expect(order.errors[:email]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -145,7 +145,7 @@ describe Spree::Order::Checkout do
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for #2028
|
||||
# Regression test for Spree #2028
|
||||
context "when payment is not required" do
|
||||
before do
|
||||
allow(order).to receive_messages payment_required?: false
|
||||
@@ -209,7 +209,7 @@ describe Spree::Order::Checkout do
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for #3665
|
||||
# Regression test for Spree #3665
|
||||
context "with only a complete step" do
|
||||
before do
|
||||
@old_checkout_flow = Spree::Order.checkout_flow
|
||||
|
||||
57
spec/models/spree/order/payment_spec.rb
Normal file
57
spec/models/spree/order/payment_spec.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Spree::Order do
|
||||
let(:order) { build(:order) }
|
||||
let(:updater) { OrderManagement::Order::Updater.new(order) }
|
||||
let(:bogus) { create(:bogus_payment_method, distributors: [create(:enterprise)]) }
|
||||
|
||||
before do
|
||||
# So that Payment#purchase! is called during processing
|
||||
Spree::Config[:auto_capture] = true
|
||||
|
||||
allow(order).to receive_message_chain(:line_items, :empty?).and_return(false)
|
||||
allow(order).to receive_messages total: 100
|
||||
end
|
||||
|
||||
it 'processes all payments' do
|
||||
payment1 = create(:payment, amount: 50, payment_method: bogus)
|
||||
payment2 = create(:payment, amount: 50, payment_method: bogus)
|
||||
allow(order).to receive(:pending_payments).and_return([payment1, payment2])
|
||||
|
||||
order.process_payments!
|
||||
updater.update_payment_state
|
||||
expect(order.payment_state).to eq 'paid'
|
||||
|
||||
expect(payment1).to be_completed
|
||||
expect(payment2).to be_completed
|
||||
end
|
||||
|
||||
it 'does not go over total for order' do
|
||||
payment1 = create(:payment, amount: 50, payment_method: bogus)
|
||||
payment2 = create(:payment, amount: 50, payment_method: bogus)
|
||||
payment3 = create(:payment, amount: 50, payment_method: bogus)
|
||||
allow(order).to receive(:pending_payments).and_return([payment1, payment2, payment3])
|
||||
|
||||
order.process_payments!
|
||||
updater.update_payment_state
|
||||
expect(order.payment_state).to eq 'paid'
|
||||
|
||||
expect(payment1).to be_completed
|
||||
expect(payment2).to be_completed
|
||||
expect(payment3).to be_checkout
|
||||
end
|
||||
|
||||
it "does not use failed payments" do
|
||||
payment1 = create(:payment, amount: 50, payment_method: bogus)
|
||||
payment2 = create(:payment, amount: 50, state: 'failed', payment_method: bogus)
|
||||
allow(order).to receive(:pending_payments).and_return([payment1])
|
||||
|
||||
expect(payment2).not_to receive(:process!)
|
||||
|
||||
order.process_payments!
|
||||
end
|
||||
end
|
||||
end
|
||||
182
spec/models/spree/order/state_machine_spec.rb
Normal file
182
spec/models/spree/order/state_machine_spec.rb
Normal file
@@ -0,0 +1,182 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Order do
|
||||
let(:order) { Spree::Order.new }
|
||||
before do
|
||||
# Ensure state machine has been re-defined correctly
|
||||
Spree::Order.define_state_machine!
|
||||
# We don't care about this validation here
|
||||
allow(order).to receive(:require_email)
|
||||
end
|
||||
|
||||
context "#next!" do
|
||||
context "when current state is payment" do
|
||||
before do
|
||||
order.state = "payment"
|
||||
order.run_callbacks(:create)
|
||||
allow(order).to receive_messages payment_required?: true
|
||||
allow(order).to receive_messages process_payments!: true
|
||||
allow(order).to receive :has_available_shipment
|
||||
end
|
||||
|
||||
context "when payment processing succeeds" do
|
||||
before { allow(order).to receive_messages process_payments!: true }
|
||||
|
||||
it "should finalize order when transitioning to complete state" do
|
||||
expect(order).to receive(:finalize!)
|
||||
order.next!
|
||||
end
|
||||
|
||||
context "when credit card processing fails" do
|
||||
before { allow(order).to receive_messages process_payments!: false }
|
||||
|
||||
it "should not complete the order" do
|
||||
order.next
|
||||
expect(order.state).to eq "payment"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when payment processing fails" do
|
||||
before { allow(order).to receive_messages process_payments!: false }
|
||||
|
||||
it "cannot transition to complete" do
|
||||
order.next
|
||||
expect(order.state).to eq "payment"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when current state is address" do
|
||||
before do
|
||||
allow(order).to receive(:has_available_payment)
|
||||
allow(order).to receive(:ensure_available_shipping_rates)
|
||||
order.state = "address"
|
||||
end
|
||||
|
||||
it "adjusts tax rates when transitioning to delivery" do
|
||||
# Once because the record is being saved
|
||||
# Twice because it is transitioning to the delivery state
|
||||
expect(Spree::TaxRate).to receive(:adjust).twice
|
||||
order.next!
|
||||
end
|
||||
end
|
||||
|
||||
context "when current state is delivery" do
|
||||
before do
|
||||
order.state = "delivery"
|
||||
allow(order).to receive_messages total: 10.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#can_cancel?" do
|
||||
%w(pending backorder ready).each do |shipment_state|
|
||||
it "should be true if shipment_state is #{shipment_state}" do
|
||||
allow(order).to receive_messages completed?: true
|
||||
order.shipment_state = shipment_state
|
||||
expect(order.can_cancel?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
(Spree::Shipment.state_machine.states.keys - %w(pending backorder ready)).each do |shipment_state|
|
||||
it "should be false if shipment_state is #{shipment_state}" do
|
||||
allow(order).to receive_messages completed?: true
|
||||
order.shipment_state = shipment_state
|
||||
expect(order.can_cancel?).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#cancel" do
|
||||
let!(:variant) { build(:variant) }
|
||||
let!(:inventory_units) {
|
||||
[build(:inventory_unit, variant: variant),
|
||||
build(:inventory_unit, variant: variant)]
|
||||
}
|
||||
let!(:shipment) do
|
||||
shipment = build(:shipment)
|
||||
allow(shipment).to receive_messages inventory_units: inventory_units
|
||||
allow(order).to receive_messages shipments: [shipment]
|
||||
shipment
|
||||
end
|
||||
|
||||
before do
|
||||
allow(order).to receive_messages line_items: [build(:line_item, variant: variant, quantity: 2)]
|
||||
allow(order.line_items).to receive_messages find_by_variant_id: order.line_items.first
|
||||
|
||||
allow(order).to receive_messages completed?: true
|
||||
allow(order).to receive_messages allow_cancel?: true
|
||||
end
|
||||
|
||||
it "should send a cancel email" do
|
||||
# Stub methods that cause side-effects in this test
|
||||
allow(shipment).to receive(:cancel!)
|
||||
allow(order).to receive :has_available_shipment
|
||||
allow(order).to receive :restock_items!
|
||||
mail_message = double "Mail::Message"
|
||||
order_id = nil
|
||||
expect(Spree::OrderMailer).to receive(:cancel_email) { |*args|
|
||||
order_id = args[0]
|
||||
mail_message
|
||||
}
|
||||
expect(mail_message).to receive :deliver
|
||||
order.cancel!
|
||||
expect(order_id).to eq order.id
|
||||
end
|
||||
|
||||
context "restocking inventory" do
|
||||
before do
|
||||
allow(shipment).to receive(:ensure_correct_adjustment)
|
||||
allow(shipment).to receive(:update_order)
|
||||
allow(Spree::OrderMailer).to receive(:cancel_email).and_return(mail_message = double)
|
||||
allow(mail_message).to receive :deliver
|
||||
|
||||
allow(order).to receive :has_available_shipment
|
||||
end
|
||||
end
|
||||
|
||||
context "resets payment state" do
|
||||
before do
|
||||
# Stubs methods that cause unwanted side effects in this test
|
||||
allow(Spree::OrderMailer).to receive(:cancel_email).and_return(mail_message = double)
|
||||
allow(mail_message).to receive :deliver
|
||||
allow(order).to receive :has_available_shipment
|
||||
allow(order).to receive :restock_items!
|
||||
allow(shipment).to receive(:cancel!)
|
||||
end
|
||||
|
||||
context "without shipped items" do
|
||||
it "should set payment state to 'credit owed'" do
|
||||
order.cancel!
|
||||
expect(order.payment_state).to eq 'credit_owed'
|
||||
end
|
||||
end
|
||||
|
||||
context "with shipped items" do
|
||||
before do
|
||||
allow(order).to receive_messages shipment_state: 'partial'
|
||||
end
|
||||
|
||||
it "should not alter the payment state" do
|
||||
order.cancel!
|
||||
expect(order.payment_state).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Another regression test for Spree #729
|
||||
context "#resume" do
|
||||
before do
|
||||
allow(order).to receive_messages email: "user@spreecommerce.com"
|
||||
allow(order).to receive_messages state: "canceled"
|
||||
allow(order).to receive_messages allow_resume?: true
|
||||
|
||||
# Stubs method that cause unwanted side effects in this test
|
||||
allow(order).to receive :has_available_shipment
|
||||
end
|
||||
end
|
||||
end
|
||||
117
spec/models/spree/order/tax_spec.rb
Normal file
117
spec/models/spree/order/tax_spec.rb
Normal file
@@ -0,0 +1,117 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Spree::Order do
|
||||
let(:order) { build(:order) }
|
||||
|
||||
context "#tax_zone" do
|
||||
let(:bill_address) { create :address }
|
||||
let(:ship_address) { create :address }
|
||||
let(:order) { Spree::Order.create(ship_address: ship_address, bill_address: bill_address) }
|
||||
let(:zone) { create :zone }
|
||||
|
||||
context "when no zones exist" do
|
||||
before { Spree::Zone.destroy_all }
|
||||
|
||||
it "should return nil" do
|
||||
expect(order.tax_zone).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when :tax_using_ship_address => true" do
|
||||
before { Spree::Config.set(tax_using_ship_address: true) }
|
||||
|
||||
it "should calculate using ship_address" do
|
||||
expect(Spree::Zone).to receive(:match).at_least(:once).with(ship_address)
|
||||
expect(Spree::Zone).not_to receive(:match).with(bill_address)
|
||||
order.tax_zone
|
||||
end
|
||||
end
|
||||
|
||||
context "when :tax_using_ship_address => false" do
|
||||
before { Spree::Config.set(tax_using_ship_address: false) }
|
||||
|
||||
it "should calculate using bill_address" do
|
||||
expect(Spree::Zone).to receive(:match).at_least(:once).with(bill_address)
|
||||
expect(Spree::Zone).not_to receive(:match).with(ship_address)
|
||||
order.tax_zone
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is a default tax zone" do
|
||||
before do
|
||||
@default_zone = create(:zone, name: "foo_zone")
|
||||
allow(Spree::Zone).to receive_messages default_tax: @default_zone
|
||||
end
|
||||
|
||||
context "when there is a matching zone" do
|
||||
before { allow(Spree::Zone).to receive_messages(match: zone) }
|
||||
|
||||
it "should return the matching zone" do
|
||||
expect(order.tax_zone).to eq zone
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is no matching zone" do
|
||||
before { allow(Spree::Zone).to receive_messages(match: nil) }
|
||||
|
||||
it "should return the default tax zone" do
|
||||
expect(order.tax_zone).to eq @default_zone
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when no default tax zone" do
|
||||
before { allow(Spree::Zone).to receive_messages default_tax: nil }
|
||||
|
||||
context "when there is a matching zone" do
|
||||
before { allow(Spree::Zone).to receive_messages(match: zone) }
|
||||
|
||||
it "should return the matching zone" do
|
||||
expect(order.tax_zone).to eq zone
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is no matching zone" do
|
||||
before { allow(Spree::Zone).to receive_messages(match: nil) }
|
||||
|
||||
it "should return nil" do
|
||||
expect(order.tax_zone).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#exclude_tax?" do
|
||||
before do
|
||||
@order = create(:order)
|
||||
@default_zone = create(:zone)
|
||||
allow(Spree::Zone).to receive_messages default_tax: @default_zone
|
||||
end
|
||||
|
||||
context "when prices include tax" do
|
||||
before { Spree::Config.set(prices_inc_tax: true) }
|
||||
|
||||
it "should be true when tax_zone is not the same as the default" do
|
||||
allow(@order).to receive_messages tax_zone: create(:zone, name: "other_zone")
|
||||
expect(@order.exclude_tax?).to be_truthy
|
||||
end
|
||||
|
||||
it "should be false when tax_zone is the same as the default" do
|
||||
allow(@order).to receive_messages tax_zone: @default_zone
|
||||
expect(@order.exclude_tax?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context "when prices do not include tax" do
|
||||
before { Spree::Config.set(prices_inc_tax: false) }
|
||||
|
||||
it "should be false" do
|
||||
expect(@order.exclude_tax?).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
20
spec/models/spree/order/updating_spec.rb
Normal file
20
spec/models/spree/order/updating_spec.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Order do
|
||||
let(:order) { build(:order) }
|
||||
|
||||
context "#update!" do
|
||||
let(:line_items) { [build(:line_item, amount: 5)] }
|
||||
|
||||
context "when there are update hooks" do
|
||||
before { Spree::Order.register_update_hook :foo }
|
||||
after { Spree::Order.update_hooks.clear }
|
||||
it "should call each of the update hooks" do
|
||||
expect(order).to receive :foo
|
||||
order.update!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
92
spec/models/spree/order_contents_spec.rb
Normal file
92
spec/models/spree/order_contents_spec.rb
Normal file
@@ -0,0 +1,92 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::OrderContents do
|
||||
let(:order) { Spree::Order.create }
|
||||
subject { described_class.new(order) }
|
||||
|
||||
context "#add" do
|
||||
let(:variant) { create(:variant) }
|
||||
|
||||
context 'given quantity is not explicitly provided' do
|
||||
it 'should add one line item' do
|
||||
line_item = subject.add(variant)
|
||||
expect(line_item.quantity).to eq 1
|
||||
expect(order.line_items.size).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
it 'should add line item if one does not exist' do
|
||||
line_item = subject.add(variant, 1)
|
||||
expect(line_item.quantity).to eq 1
|
||||
expect(order.line_items.size).to eq 1
|
||||
end
|
||||
|
||||
it 'should update line item if one exists' do
|
||||
subject.add(variant, 1)
|
||||
line_item = subject.add(variant, 1)
|
||||
expect(line_item.quantity).to eq 2
|
||||
expect(order.line_items.size).to eq 1
|
||||
end
|
||||
|
||||
it "should update order totals" do
|
||||
expect(order.item_total.to_f).to eq 0.00
|
||||
expect(order.total.to_f).to eq 0.00
|
||||
|
||||
subject.add(variant, 1)
|
||||
|
||||
expect(order.item_total.to_f).to eq 19.99
|
||||
expect(order.total.to_f).to eq 19.99
|
||||
end
|
||||
end
|
||||
|
||||
context "#remove" do
|
||||
let(:variant) { create(:variant) }
|
||||
|
||||
context "given an invalid variant" do
|
||||
it "raises an exception" do
|
||||
expect {
|
||||
subject.remove(variant, 1)
|
||||
}.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given quantity is not explicitly provided' do
|
||||
it 'should remove one line item' do
|
||||
line_item = subject.add(variant, 3)
|
||||
subject.remove(variant)
|
||||
|
||||
expect(line_item.reload.quantity).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
it 'should reduce line_item quantity if quantity is less the line_item quantity' do
|
||||
line_item = subject.add(variant, 3)
|
||||
subject.remove(variant, 1)
|
||||
|
||||
expect(line_item.reload.quantity).to eq 2
|
||||
end
|
||||
|
||||
it 'should remove line_item if quantity matches line_item quantity' do
|
||||
subject.add(variant, 1)
|
||||
subject.remove(variant, 1)
|
||||
|
||||
expect(order.reload.find_line_item_by_variant(variant)).to be_nil
|
||||
end
|
||||
|
||||
it "should update order totals" do
|
||||
expect(order.item_total.to_f).to eq 0.00
|
||||
expect(order.total.to_f).to eq 0.00
|
||||
|
||||
subject.add(variant, 2)
|
||||
|
||||
expect(order.item_total.to_f).to eq 39.98
|
||||
expect(order.total.to_f).to eq 39.98
|
||||
|
||||
subject.remove(variant, 1)
|
||||
expect(order.item_total.to_f).to eq 19.99
|
||||
expect(order.total.to_f).to eq 19.99
|
||||
end
|
||||
end
|
||||
end
|
||||
144
spec/models/spree/order_inventory_spec.rb
Normal file
144
spec/models/spree/order_inventory_spec.rb
Normal file
@@ -0,0 +1,144 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::OrderInventory do
|
||||
let(:order) { create :completed_order_with_totals }
|
||||
let(:line_item) { order.line_items.first }
|
||||
subject { described_class.new(order) }
|
||||
|
||||
it 'inventory_units_for should return array of units for a given variant' do
|
||||
units = subject.inventory_units_for(line_item.variant)
|
||||
expect(units.map(&:variant_id)).to eq [line_item.variant.id]
|
||||
end
|
||||
|
||||
context "when order is missing inventory units" do
|
||||
before do
|
||||
line_item.update_column(:quantity, 2)
|
||||
end
|
||||
|
||||
it 'should be a messed up order' do
|
||||
expect(order.shipments.first.inventory_units_for(line_item.variant).size).to eq 1
|
||||
expect(line_item.reload.quantity).to eq 2
|
||||
end
|
||||
|
||||
it 'should increase the number of inventory units' do
|
||||
subject.verify(line_item)
|
||||
expect(order.reload.shipments.first.inventory_units_for(line_item.variant).size).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
context "#add_to_shipment" do
|
||||
let(:shipment) { order.shipments.first }
|
||||
let(:variant) { create :variant }
|
||||
|
||||
context "order is not completed" do
|
||||
before { allow(order).to receive_messages completed?: false }
|
||||
|
||||
it "doesn't unstock items" do
|
||||
expect(shipment.stock_location).not_to receive(:unstock)
|
||||
expect(subject.send(:add_to_shipment, shipment, variant, 5)).to eq 5
|
||||
end
|
||||
end
|
||||
|
||||
it 'should create inventory_units in the necessary states' do
|
||||
expect(shipment.stock_location).to receive(:fill_status).with(variant, 5).and_return([3, 2])
|
||||
|
||||
expect(subject.send(:add_to_shipment, shipment, variant, 5)).to eq 5
|
||||
|
||||
units = shipment.inventory_units.group_by &:variant_id
|
||||
units = units[variant.id].group_by &:state
|
||||
expect(units['backordered'].size).to eq 2
|
||||
expect(units['on_hand'].size).to eq 3
|
||||
end
|
||||
|
||||
it 'should create stock_movement' do
|
||||
expect(subject.send(:add_to_shipment, shipment, variant, 5)).to eq 5
|
||||
|
||||
stock_item = shipment.stock_location.stock_item(variant)
|
||||
movement = stock_item.stock_movements.last
|
||||
expect(movement.quantity).to eq(-5)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when order has too many inventory units' do
|
||||
before do
|
||||
line_item.quantity = 3
|
||||
line_item.save!
|
||||
|
||||
line_item.update_column(:quantity, 2)
|
||||
order.reload
|
||||
end
|
||||
|
||||
it 'should be a messed up order' do
|
||||
expect(order.shipments.first.inventory_units_for(line_item.variant).size).to eq 3
|
||||
expect(line_item.quantity).to eq 2
|
||||
end
|
||||
|
||||
it 'should decrease the number of inventory units' do
|
||||
subject.verify(line_item)
|
||||
expect(order.reload.shipments.first.inventory_units_for(line_item.variant).size).to eq 2
|
||||
end
|
||||
|
||||
context '#remove_from_shipment' do
|
||||
let(:shipment) { order.shipments.first }
|
||||
let(:variant) { order.line_items.first.variant }
|
||||
|
||||
context "order is not completed" do
|
||||
before { allow(order).to receive_messages completed?: false }
|
||||
|
||||
it "doesn't restock items" do
|
||||
expect(shipment.stock_location).not_to receive(:restock)
|
||||
expect(subject.send(:remove_from_shipment, shipment, variant, 1)).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
it 'should create stock_movement' do
|
||||
expect(subject.send(:remove_from_shipment, shipment, variant, 1)).to eq 1
|
||||
|
||||
stock_item = shipment.stock_location.stock_item(variant)
|
||||
movement = stock_item.stock_movements.last
|
||||
expect(movement.quantity).to eq 1
|
||||
end
|
||||
|
||||
it 'should destroy backordered units first' do
|
||||
allow(shipment).to receive_messages(inventory_units_for: [build(:inventory_unit, variant_id: variant.id, state: 'backordered'),
|
||||
build(:inventory_unit, variant_id: variant.id, state: 'on_hand'),
|
||||
build(:inventory_unit, variant_id: variant.id, state: 'backordered')])
|
||||
|
||||
expect(shipment.inventory_units_for[0]).to receive(:destroy)
|
||||
expect(shipment.inventory_units_for[1]).not_to receive(:destroy)
|
||||
expect(shipment.inventory_units_for[2]).to receive(:destroy)
|
||||
|
||||
expect(subject.send(:remove_from_shipment, shipment, variant, 2)).to eq 2
|
||||
end
|
||||
|
||||
it 'should destroy unshipped units first' do
|
||||
allow(shipment).to receive_messages(inventory_units_for: [build(:inventory_unit, variant_id: variant.id, state: 'shipped'),
|
||||
build(:inventory_unit, variant_id: variant.id, state: 'on_hand')] )
|
||||
|
||||
expect(shipment.inventory_units_for[0]).not_to receive(:destroy)
|
||||
expect(shipment.inventory_units_for[1]).to receive(:destroy)
|
||||
|
||||
expect(subject.send(:remove_from_shipment, shipment, variant, 1)).to eq 1
|
||||
end
|
||||
|
||||
it 'only attempts to destroy as many units as are eligible, and return amount destroyed' do
|
||||
allow(shipment).to receive_messages(inventory_units_for: [build(:inventory_unit, variant_id: variant.id, state: 'shipped'),
|
||||
build(:inventory_unit, variant_id: variant.id, state: 'on_hand')] )
|
||||
|
||||
expect(shipment.inventory_units_for[0]).not_to receive(:destroy)
|
||||
expect(shipment.inventory_units_for[1]).to receive(:destroy)
|
||||
|
||||
expect(subject.send(:remove_from_shipment, shipment, variant, 1)).to eq 1
|
||||
end
|
||||
|
||||
it 'should destroy self if not inventory units remain' do
|
||||
allow(shipment.inventory_units).to receive_messages(count: 0)
|
||||
expect(shipment).to receive(:destroy)
|
||||
|
||||
expect(subject.send(:remove_from_shipment, shipment, variant, 1)).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,11 +3,498 @@ require 'spec_helper'
|
||||
describe Spree::Order do
|
||||
include OpenFoodNetwork::EmailHelper
|
||||
|
||||
let(:user) { build(:user, email: "spree@example.com") }
|
||||
let(:order) { build(:order, user: user) }
|
||||
|
||||
context "#products" do
|
||||
let(:order) { create(:order_with_line_items) }
|
||||
|
||||
it "should return ordered products" do
|
||||
expect(order.products.first).to eq order.line_items.first.product
|
||||
end
|
||||
|
||||
it "contains?" do
|
||||
expect(order.contains?(order.line_items.first.variant)).to be_truthy
|
||||
end
|
||||
|
||||
it "can find a line item matching a given variant" do
|
||||
expect(order.find_line_item_by_variant(order.line_items.third.variant)).to_not be_nil
|
||||
expect(order.find_line_item_by_variant(build(:variant))).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "#generate_order_number" do
|
||||
it "should generate a random string" do
|
||||
expect(order.generate_order_number.is_a?(String)).to be_truthy
|
||||
expect((!order.generate_order_number.to_s.empty?)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "#associate_user!" do
|
||||
it "should associate a user with a persisted order" do
|
||||
order = create(:order_with_line_items, created_by: nil)
|
||||
user = create(:user)
|
||||
|
||||
order.user = nil
|
||||
order.email = nil
|
||||
order.associate_user!(user)
|
||||
expect(order.user).to eq user
|
||||
expect(order.email).to eq user.email
|
||||
expect(order.created_by).to eq user
|
||||
|
||||
# verify that the changes we made were persisted
|
||||
order.reload
|
||||
expect(order.user).to eq user
|
||||
expect(order.email).to eq user.email
|
||||
expect(order.created_by).to eq user
|
||||
end
|
||||
|
||||
it "should not overwrite the created_by if it already is set" do
|
||||
creator = create(:user)
|
||||
order = create(:order_with_line_items, created_by: creator)
|
||||
user = create(:user)
|
||||
|
||||
order.user = nil
|
||||
order.email = nil
|
||||
order.associate_user!(user)
|
||||
expect(order.user).to eq user
|
||||
expect(order.email).to eq user.email
|
||||
expect(order.created_by).to eq creator
|
||||
|
||||
# verify that the changes we made were persisted
|
||||
order.reload
|
||||
expect(order.user).to eq user
|
||||
expect(order.email).to eq user.email
|
||||
expect(order.created_by).to eq creator
|
||||
end
|
||||
|
||||
it "should associate a user with a non-persisted order" do
|
||||
order = Spree::Order.new
|
||||
|
||||
expect do
|
||||
order.associate_user!(user)
|
||||
end.to change { [order.user, order.email] }.from([nil, nil]).to([user, user.email])
|
||||
end
|
||||
|
||||
it "should not persist an invalid address" do
|
||||
address = Spree::Address.new
|
||||
order.user = nil
|
||||
order.email = nil
|
||||
order.ship_address = address
|
||||
expect do
|
||||
order.associate_user!(user)
|
||||
end.not_to change { address.persisted? }.from(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "#create" do
|
||||
it "should assign an order number" do
|
||||
order = Spree::Order.create
|
||||
expect(order.number).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "#can_ship?" do
|
||||
let(:order) { Spree::Order.create }
|
||||
|
||||
it "should be true for order in the 'complete' state" do
|
||||
allow(order).to receive_messages(complete?: true)
|
||||
expect(order.can_ship?).to be_truthy
|
||||
end
|
||||
|
||||
it "should be true for order in the 'resumed' state" do
|
||||
allow(order).to receive_messages(resumed?: true)
|
||||
expect(order.can_ship?).to be_truthy
|
||||
end
|
||||
|
||||
it "should be true for an order in the 'awaiting return' state" do
|
||||
allow(order).to receive_messages(awaiting_return?: true)
|
||||
expect(order.can_ship?).to be_truthy
|
||||
end
|
||||
|
||||
it "should be true for an order in the 'returned' state" do
|
||||
allow(order).to receive_messages(returned?: true)
|
||||
expect(order.can_ship?).to be_truthy
|
||||
end
|
||||
|
||||
it "should be false if the order is neither in the 'complete' nor 'resumed' state" do
|
||||
allow(order).to receive_messages(resumed?: false, complete?: false)
|
||||
expect(order.can_ship?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context "checking if order is paid" do
|
||||
context "payment_state is paid" do
|
||||
before { allow(order).to receive_messages payment_state: 'paid' }
|
||||
it { expect(order).to be_paid }
|
||||
end
|
||||
|
||||
context "payment_state is credit_owned" do
|
||||
before { allow(order).to receive_messages payment_state: 'credit_owed' }
|
||||
it { expect(order).to be_paid }
|
||||
end
|
||||
end
|
||||
|
||||
context "#finalize!" do
|
||||
let(:order) { Spree::Order.create }
|
||||
it "should set completed_at" do
|
||||
expect(order).to receive(:touch).with(:completed_at)
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
it "should sell inventory units" do
|
||||
order.shipments.each do |shipment|
|
||||
expect(shipment).to receive(:update!)
|
||||
expect(shipment).to receive(:finalize!)
|
||||
end
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
it "should decrease the stock for each variant in the shipment" do
|
||||
order.shipments.each do |shipment|
|
||||
expect(shipment.stock_location).to receive(:decrease_stock_for_variant)
|
||||
end
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
it "should change the shipment state to ready if order is paid" do
|
||||
Spree::Shipment.create(order: order)
|
||||
order.shipments.reload
|
||||
|
||||
allow(order).to receive_messages(paid?: true, complete?: true)
|
||||
order.finalize!
|
||||
order.reload # reload so we're sure the changes are persisted
|
||||
expect(order.shipment_state).to eq 'ready'
|
||||
end
|
||||
|
||||
it "should send an order confirmation email" do
|
||||
expect do
|
||||
order.finalize!
|
||||
end.to enqueue_job ConfirmOrderJob
|
||||
end
|
||||
|
||||
it "should freeze all adjustments" do
|
||||
# Stub this method as it's called due to a callback
|
||||
# and it's irrelevant to this test
|
||||
allow(order).to receive :has_available_shipment
|
||||
allow(Spree::OrderMailer).to receive_message_chain :confirm_email, :deliver
|
||||
adjustments = double
|
||||
allow(order).to receive_messages adjustments: adjustments
|
||||
expect(adjustments).to receive(:update_all).with(state: 'closed')
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
it "should log state event" do
|
||||
expect(order.state_changes).to receive(:create).exactly(3).times # order, shipment & payment state changes
|
||||
order.finalize!
|
||||
end
|
||||
|
||||
it 'calls updater#before_save' do
|
||||
expect(order.updater).to receive(:before_save_hook)
|
||||
order.finalize!
|
||||
end
|
||||
end
|
||||
|
||||
context "#process_payments!" do
|
||||
let(:payment) { build(:payment) }
|
||||
before { allow(order).to receive_messages pending_payments: [payment], total: 10 }
|
||||
|
||||
it "should process the payments" do
|
||||
expect(payment).to receive(:process!)
|
||||
expect(order.process_payments!).to be_truthy
|
||||
end
|
||||
|
||||
it "should return false if no pending_payments available" do
|
||||
allow(order).to receive_messages pending_payments: []
|
||||
expect(order.process_payments!).to be_falsy
|
||||
end
|
||||
|
||||
context "when a payment raises a GatewayError" do
|
||||
before { expect(payment).to receive(:process!).and_raise(Spree::Core::GatewayError) }
|
||||
|
||||
it "should return true when configured to allow checkout on gateway failures" do
|
||||
Spree::Config.set allow_checkout_on_gateway_error: true
|
||||
expect(order.process_payments!).to be_truthy
|
||||
end
|
||||
|
||||
it "should return false when not configured to allow checkout on gateway failures" do
|
||||
Spree::Config.set allow_checkout_on_gateway_error: false
|
||||
expect(order.process_payments!).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#outstanding_balance" do
|
||||
it "should return positive amount when payment_total is less than total" do
|
||||
order.payment_total = 20.20
|
||||
order.total = 30.30
|
||||
expect(order.outstanding_balance).to eq 10.10
|
||||
end
|
||||
it "should return negative amount when payment_total is greater than total" do
|
||||
order.total = 8.20
|
||||
order.payment_total = 10.20
|
||||
expect(order.outstanding_balance).to be_within(0.001).of(-2.00)
|
||||
end
|
||||
end
|
||||
|
||||
context "#outstanding_balance?" do
|
||||
it "should be true when total greater than payment_total" do
|
||||
order.total = 10.10
|
||||
order.payment_total = 9.50
|
||||
expect(order.outstanding_balance?).to be_truthy
|
||||
end
|
||||
it "should be true when total less than payment_total" do
|
||||
order.total = 8.25
|
||||
order.payment_total = 10.44
|
||||
expect(order.outstanding_balance?).to be_truthy
|
||||
end
|
||||
it "should be false when total equals payment_total" do
|
||||
order.total = 10.10
|
||||
order.payment_total = 10.10
|
||||
expect(order.outstanding_balance?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context "#completed?" do
|
||||
it "should indicate if order is completed" do
|
||||
order.completed_at = nil
|
||||
expect(order.completed?).to be_falsy
|
||||
|
||||
order.completed_at = Time.zone.now
|
||||
expect(order.completed?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "#allow_checkout?" do
|
||||
it "should be true if there are line_items in the order" do
|
||||
allow(order).to receive_message_chain(:line_items, count: 1)
|
||||
expect(order.checkout_allowed?).to be_truthy
|
||||
end
|
||||
it "should be false if there are no line_items in the order" do
|
||||
allow(order).to receive_message_chain(:line_items, count: 0)
|
||||
expect(order.checkout_allowed?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context "#item_count" do
|
||||
before do
|
||||
@order = create(:order, user: user)
|
||||
@order.line_items = [create(:line_item, quantity: 2), create(:line_item, quantity: 1)]
|
||||
end
|
||||
it "should return the correct number of items" do
|
||||
expect(@order.item_count).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
context "#amount" do
|
||||
before do
|
||||
@order = create(:order, user: user)
|
||||
@order.line_items = [create(:line_item, price: 1.0, quantity: 2),
|
||||
create(:line_item, price: 1.0, quantity: 1)]
|
||||
end
|
||||
it "should return the correct lum sum of items" do
|
||||
expect(@order.amount).to eq 3.0
|
||||
end
|
||||
end
|
||||
|
||||
context "#can_cancel?" do
|
||||
it "should be false for completed order in the canceled state" do
|
||||
order.state = 'canceled'
|
||||
order.shipment_state = 'ready'
|
||||
order.completed_at = Time.zone.now
|
||||
expect(order.can_cancel?).to be_falsy
|
||||
end
|
||||
|
||||
it "should be true for completed order with no shipment" do
|
||||
order.state = 'complete'
|
||||
order.shipment_state = nil
|
||||
order.completed_at = Time.zone.now
|
||||
expect(order.can_cancel?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "insufficient_stock_lines" do
|
||||
let(:line_item) { build(:line_item) }
|
||||
|
||||
before do
|
||||
allow(order).to receive_messages(line_items: [line_item])
|
||||
allow(line_item).to receive(:insufficient_stock?) { true }
|
||||
end
|
||||
|
||||
it "should return line_item that has insufficient stock on hand" do
|
||||
expect(order.insufficient_stock_lines.size).to eq 1
|
||||
expect(order.insufficient_stock_lines.include?(line_item)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "empty!" do
|
||||
it "should clear out all line items and adjustments" do
|
||||
order = build(:order)
|
||||
allow(order).to receive_messages(line_items: line_items = [])
|
||||
allow(order).to receive_messages(adjustments: adjustments = [])
|
||||
expect(order.line_items).to receive(:destroy_all)
|
||||
expect(order.adjustments).to receive(:destroy_all)
|
||||
|
||||
order.empty!
|
||||
end
|
||||
end
|
||||
|
||||
context "#display_outstanding_balance" do
|
||||
it "returns the value as a spree money" do
|
||||
allow(order).to receive(:outstanding_balance) { 10.55 }
|
||||
expect(order.display_outstanding_balance).to eq Spree::Money.new(10.55)
|
||||
end
|
||||
end
|
||||
|
||||
context "#display_item_total" do
|
||||
it "returns the value as a spree money" do
|
||||
allow(order).to receive(:item_total) { 10.55 }
|
||||
expect(order.display_item_total).to eq Spree::Money.new(10.55)
|
||||
end
|
||||
end
|
||||
|
||||
context "#display_adjustment_total" do
|
||||
it "returns the value as a spree money" do
|
||||
order.adjustment_total = 10.55
|
||||
expect(order.display_adjustment_total).to eq Spree::Money.new(10.55)
|
||||
end
|
||||
end
|
||||
|
||||
context "#display_total" do
|
||||
it "returns the value as a spree money" do
|
||||
order.total = 10.55
|
||||
expect(order.display_total).to eq Spree::Money.new(10.55)
|
||||
end
|
||||
end
|
||||
|
||||
context "#currency" do
|
||||
context "when object currency is ABC" do
|
||||
before { order.currency = "ABC" }
|
||||
|
||||
it "returns the currency from the object" do
|
||||
expect(order.currency).to eq "ABC"
|
||||
end
|
||||
end
|
||||
|
||||
context "when object currency is nil" do
|
||||
before { order.currency = nil }
|
||||
|
||||
it "returns the globally configured currency" do
|
||||
expect(order.currency).to eq Spree::Config[:currency]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for Spree #2191
|
||||
context "when an order has an adjustment that zeroes the total, but another adjustment for shipping that raises it above zero" do
|
||||
let!(:persisted_order) { create(:order) }
|
||||
let!(:line_item) { create(:line_item) }
|
||||
let!(:shipping_method) do
|
||||
sm = create(:shipping_method)
|
||||
sm.calculator.preferred_amount = 10
|
||||
sm.save
|
||||
sm
|
||||
end
|
||||
|
||||
before do
|
||||
# Don't care about available payment methods in this test
|
||||
allow(persisted_order).to receive_messages(has_available_payment: false)
|
||||
persisted_order.line_items << line_item
|
||||
persisted_order.adjustments.create(amount: -line_item.amount, label: "Promotion")
|
||||
persisted_order.state = 'delivery'
|
||||
persisted_order.save # To ensure new state_change event
|
||||
end
|
||||
|
||||
it "transitions from delivery to payment" do
|
||||
allow(persisted_order).to receive_messages(payment_required?: true)
|
||||
persisted_order.next!
|
||||
expect(persisted_order.state).to eq "payment"
|
||||
end
|
||||
end
|
||||
|
||||
context "payment required?" do
|
||||
let(:order) { Spree::Order.new }
|
||||
|
||||
context "total is zero" do
|
||||
it { expect(order.payment_required?).to be_falsy }
|
||||
end
|
||||
|
||||
context "total > zero" do
|
||||
before { allow(order).to receive_messages(total: 1) }
|
||||
it { expect(order.payment_required?).to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
context "add_update_hook" do
|
||||
before do
|
||||
Spree::Order.class_eval do
|
||||
register_update_hook :add_awesome_sauce
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
Spree::Order.update_hooks = Set.new
|
||||
end
|
||||
|
||||
it "calls hook during update" do
|
||||
order = create(:order)
|
||||
expect(order).to receive(:add_awesome_sauce)
|
||||
order.update!
|
||||
end
|
||||
|
||||
it "calls hook during finalize" do
|
||||
order = create(:order)
|
||||
expect(order).to receive(:add_awesome_sauce)
|
||||
order.finalize!
|
||||
end
|
||||
end
|
||||
|
||||
context "ensure shipments will be updated" do
|
||||
before { Spree::Shipment.create!(order: order) }
|
||||
|
||||
it "destroys current shipments" do
|
||||
order.ensure_updated_shipments
|
||||
expect(order.shipments).to be_empty
|
||||
end
|
||||
|
||||
it "puts order back in address state" do
|
||||
order.ensure_updated_shipments
|
||||
expect(order.state).to eql "address"
|
||||
end
|
||||
end
|
||||
|
||||
describe ".tax_address" do
|
||||
before { Spree::Config[:tax_using_ship_address] = tax_using_ship_address }
|
||||
subject { order.tax_address }
|
||||
|
||||
context "when tax_using_ship_address is true" do
|
||||
let(:tax_using_ship_address) { true }
|
||||
|
||||
it 'returns ship_address' do
|
||||
expect(subject).to eq order.ship_address
|
||||
end
|
||||
end
|
||||
|
||||
context "when tax_using_ship_address is not true" do
|
||||
let(:tax_using_ship_address) { false }
|
||||
|
||||
it "returns bill_address" do
|
||||
expect(subject).to eq order.bill_address
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '#updater' do
|
||||
it 'returns an OrderManagement::Order::Updater' do
|
||||
expect(order.updater.class).to eq OrderManagement::Order::Updater
|
||||
end
|
||||
end
|
||||
|
||||
describe "email validation" do
|
||||
let(:order) { build(:order) }
|
||||
|
||||
it "has errors if email is blank" do
|
||||
order.stub(require_email: true)
|
||||
allow(order).to receive_messages(require_email: true)
|
||||
order.email = ""
|
||||
|
||||
order.valid?
|
||||
@@ -15,7 +502,7 @@ describe Spree::Order do
|
||||
end
|
||||
|
||||
it "has errors if email is invalid" do
|
||||
order.stub(require_email: true)
|
||||
allow(order).to receive_messages(require_email: true)
|
||||
order.email = "invalid_email"
|
||||
|
||||
order.valid?
|
||||
@@ -23,7 +510,7 @@ describe Spree::Order do
|
||||
end
|
||||
|
||||
it "has errors if email has invalid domain" do
|
||||
order.stub(require_email: true)
|
||||
allow(order).to receive_messages(require_email: true)
|
||||
order.email = "single_letter_tld@domain.z"
|
||||
|
||||
order.valid?
|
||||
@@ -31,7 +518,7 @@ describe Spree::Order do
|
||||
end
|
||||
|
||||
it "is valid if email is valid" do
|
||||
order.stub(require_email: true)
|
||||
allow(order).to receive_messages(require_email: true)
|
||||
order.email = "a@b.ca"
|
||||
|
||||
order.valid?
|
||||
|
||||
137
spec/models/spree/return_authorization_spec.rb
Normal file
137
spec/models/spree/return_authorization_spec.rb
Normal file
@@ -0,0 +1,137 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::ReturnAuthorization do
|
||||
let(:order) { create(:shipped_order) }
|
||||
let(:variant) { order.shipments.first.inventory_units.first.variant }
|
||||
let(:return_authorization) { Spree::ReturnAuthorization.new(order: order) }
|
||||
|
||||
context "save" do
|
||||
it "should be invalid when order has no inventory units" do
|
||||
order.shipments.destroy_all
|
||||
return_authorization.save
|
||||
expect(return_authorization.errors[:order]).to eq ["has no shipped units"]
|
||||
end
|
||||
|
||||
it "should generate RMA number" do
|
||||
expect(return_authorization).to receive(:generate_number)
|
||||
return_authorization.save
|
||||
end
|
||||
end
|
||||
|
||||
context "add_variant" do
|
||||
context "on empty rma" do
|
||||
it "should associate inventory unit" do
|
||||
return_authorization.add_variant(variant.id, 1)
|
||||
expect(return_authorization.inventory_units.size).to eq 1
|
||||
end
|
||||
|
||||
it "should associate inventory units as shipped" do
|
||||
return_authorization.add_variant(variant.id, 1)
|
||||
expect(return_authorization.inventory_units.where(state: 'shipped').size).to eq 1
|
||||
end
|
||||
|
||||
it "should update order state" do
|
||||
expect(order).to receive(:authorize_return!)
|
||||
return_authorization.add_variant(variant.id, 1)
|
||||
end
|
||||
end
|
||||
|
||||
context "on rma that already has inventory_units" do
|
||||
before do
|
||||
return_authorization.add_variant(variant.id, 1)
|
||||
end
|
||||
|
||||
it "should not associate more inventory units than there are on the order" do
|
||||
return_authorization.add_variant(variant.id, 1)
|
||||
expect(return_authorization.inventory_units.size).to eq 1
|
||||
end
|
||||
|
||||
it "should not update order state" do
|
||||
expect{ return_authorization.add_variant(variant.id, 1) }.to_not change{ order.state }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "can_receive?" do
|
||||
it "should allow_receive when inventory units assigned" do
|
||||
allow(return_authorization).to receive_messages(inventory_units: [1, 2, 3])
|
||||
expect(return_authorization.can_receive?).to be_truthy
|
||||
end
|
||||
|
||||
it "should not allow_receive with no inventory units" do
|
||||
allow(return_authorization).to receive_messages(inventory_units: [])
|
||||
expect(return_authorization.can_receive?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context "receive!" do
|
||||
let(:inventory_unit) { order.shipments.first.inventory_units.first }
|
||||
|
||||
before do
|
||||
allow(return_authorization).to receive_messages(inventory_units: [inventory_unit], amount: -20)
|
||||
allow(Spree::Adjustment).to receive(:create)
|
||||
allow(order).to receive(:update!)
|
||||
end
|
||||
|
||||
it "should mark all inventory units are returned" do
|
||||
expect(inventory_unit).to receive(:return!)
|
||||
return_authorization.receive!
|
||||
end
|
||||
|
||||
it "should add credit for specified amount" do
|
||||
return_authorization.amount = 20
|
||||
mock_adjustment = double
|
||||
expect(mock_adjustment).to receive(:source=).with(return_authorization)
|
||||
expect(mock_adjustment).to receive(:adjustable=).with(order)
|
||||
expect(mock_adjustment).to receive(:save)
|
||||
expect(Spree::Adjustment).to receive(:new).with(amount: -20, label: Spree.t(:rma_credit)).and_return(mock_adjustment)
|
||||
return_authorization.receive!
|
||||
end
|
||||
|
||||
it "should update order state" do
|
||||
expect(order).to receive :update!
|
||||
return_authorization.receive!
|
||||
end
|
||||
end
|
||||
|
||||
context "force_positive_amount" do
|
||||
it "should ensure the amount is always positive" do
|
||||
return_authorization.amount = -10
|
||||
return_authorization.send :force_positive_amount
|
||||
expect(return_authorization.amount).to eq 10
|
||||
end
|
||||
end
|
||||
|
||||
context "after_save" do
|
||||
it "should run correct callbacks" do
|
||||
expect(return_authorization).to receive(:force_positive_amount)
|
||||
return_authorization.run_callbacks(:save)
|
||||
end
|
||||
end
|
||||
|
||||
context "currency" do
|
||||
before { allow(order).to receive(:currency) { "ABC" } }
|
||||
it "returns the order currency" do
|
||||
expect(return_authorization.currency).to eq "ABC"
|
||||
end
|
||||
end
|
||||
|
||||
context "display_amount" do
|
||||
it "returns a Spree::Money" do
|
||||
return_authorization.amount = 21.22
|
||||
expect(return_authorization.display_amount).to eq Spree::Money.new(21.22)
|
||||
end
|
||||
end
|
||||
|
||||
context "returnable_inventory" do
|
||||
pending "should return inventory from shipped shipments" do
|
||||
expect(return_authorization.returnable_inventory).to eq [inventory_unit]
|
||||
end
|
||||
|
||||
pending "should not return inventory from unshipped shipments" do
|
||||
expect(return_authorization.returnable_inventory).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user