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:
Luis Ramos
2020-10-15 08:49:05 +01:00
committed by GitHub
25 changed files with 3259 additions and 637 deletions

View 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

View 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

View File

@@ -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
View 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

View 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

View File

@@ -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

View 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

View 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

View 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

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
module Spree
class TokenizedPermission < ActiveRecord::Base
belongs_to :permissable, polymorphic: true
end
end

View File

@@ -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

View 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

View File

@@ -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) }

View 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

View 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

View 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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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?

View 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