Bring models related to Order from spree_core

EPIC COMMIT ALERT :-)
This commit is contained in:
Luis Ramos
2020-08-08 14:44:30 +01:00
parent 77eaebc2a7
commit 47d2f698ef
22 changed files with 2893 additions and 2 deletions

View File

@@ -0,0 +1,70 @@
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("#{self.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.where(stock_location_id: shipment.stock_location_id,
variant_id: variant_id).first
end
# Remove variant default_scope `deleted_at: nil`
def variant
Spree::Variant.unscoped { super }
end
private
def allow_ship?
Spree::Config[:allow_backorder_shipping] || self.on_hand?
end
def update_order
order.update!
end
end
end

View File

@@ -0,0 +1,102 @@
module Spree
class LineItem < ActiveRecord::Base
before_validation :adjust_quantity
belongs_to :order, class_name: "Spree::Order"
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
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
after_save :update_order
after_destroy :update_order
attr_accessor :target_shipment
def copy_price
if 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
end
def copy_tax_category
if variant
self.tax_category = variant.product.tax_category
end
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
def sufficient_stock?
Stock::Quantifier.new(variant_id).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
# Remove variant default_scope `deleted_at: nil`
def variant
Spree::Variant.unscoped { super }
end
private
def update_inventory
if changed?
Spree::OrderInventory.new(self.order).verify(self, target_shipment)
end
end
def update_order
if changed? || destroyed?
# update the order totals, etc.
order.create_tax_charge!
order.update!
end
end
end
end

575
app/models/spree/order.rb Normal file
View File

@@ -0,0 +1,575 @@
require 'spree/core/validators/email'
require 'spree/order/checkout'
module Spree
class Order < ActiveRecord::Base
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 :confirm, if: ->(order) { order.confirmation_required? }
go_to_state :complete
remove_transition from: :delivery, to: :confirm
end
token_resource
attr_reader :coupon_code
if Spree.user_class
belongs_to :user, class_name: Spree.user_class.to_s
belongs_to :created_by, class_name: Spree.user_class.to_s
else
belongs_to :user
belongs_to :created_by
end
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("#{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
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
# 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?
attr_accessor :use_billing
before_create :link_by_email
after_create :create_tax_charge!
validates :email, presence: true, if: :require_email
validates :email, email: true, if: :require_email, allow_blank: true
validate :has_available_shipment
validate :has_available_payment
make_permalink field: :number
class_attribute :update_hooks
self.update_hooks = Set.new
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)
self.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
# Is this a free order in which case the payment step should be skipped
def payment_required?
total.to_f > 0.0
end
# If true, causes the confirmation step to happen during the checkout process
def confirmation_required?
payments.map(&:payment_method).compact.any?(&:payment_profiles_supported?)
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]
return 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[self.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 ||= Spree::Config.order_updater_decorator.new(
Spree::OrderUpdater.new(self)
)
end
def update!
updater.update
end
def update_totals
updater.update_totals
end
def clone_billing_address
if bill_address and self.ship_address.nil?
self.ship_address = bill_address.clone
else
self.ship_address.attributes = bill_address.attributes.except('id', 'updated_at', 'created_at')
end
true
end
def allow_cancel?
return false unless completed? and 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? { |return_authorization| return_authorization.authorized? }
end
def contents
@contents ||= Spree::OrderContents.new(self)
end
# Associates the specified user with the order.
def associate_user!(user)
self.user = user
self.email = user.email
self.created_by = user if self.created_by.blank?
if persisted?
# immediately 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: self.created_by_id)
end
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.where(number: random).first
end
self.number = random if self.number.blank?
self.number
end
def shipped_shipments
shipments.shipped
end
def contains?(variant)
find_line_item_by_variant(variant).present?
end
def quantity_of(variant)
line_item = find_line_item_by_variant(variant)
line_item ? line_item.quantity : 0
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?
self.outstanding_balance != 0
end
def name
if (address = bill_address || ship_address)
"#{address.firstname} #{address.lastname}"
end
end
def can_ship?
self.complete? || self.resumed? || self.awaiting_return? || self.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
# lock all adjustments (coupon promotions, etc.)
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
self.state_changes.create(
previous_state: 'cart',
next_state: 'complete',
name: 'order' ,
user_id: self.user_id
)
end
def deliver_order_confirmation_email
begin
OrderMailer.confirm_email(self.id).deliver
rescue Exception => e
logger.error("#{e.class.name}: #{e.message}")
logger.error(e.backtrace * "\n")
end
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
def pending_payments
payments.select(&:checkout?)
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!
if pending_payments.empty?
raise Core::GatewayError.new Spree.t(:no_pending_payments)
else
pending_payments.each do |payment|
break if payment_total >= total
payment.process!
if payment.completed?
self.payment_total += payment.amount
end
end
end
rescue Core::GatewayError => e
result = !!Spree::Config[:allow_checkout_on_gateway_error]
errors.add(:base, e.message) and 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 merge!(order)
order.line_items.each do |line_item|
next unless line_item.currency == currency
current_line_item = self.line_items.find_by(variant: line_item.variant)
if current_line_item
current_line_item.quantity += line_item.quantity
current_line_item.save
else
line_item.order_id = self.id
line_item.save
end
end
# So that the destroy doesn't take out line items which may have been re-assigned
order.line_items.reload
order.destroy
end
def empty!
line_items.destroy_all
adjustments.destroy_all
end
def clear_adjustments!
self.adjustments.destroy_all
self.line_item_adjustments.destroy_all
end
def has_step?(step)
checkout_steps.include?(step)
end
def state_changed(name)
state = "#{name}_state"
if persisted?
old_state = self.send("#{state}_was")
self.state_changes.create(
previous_state: old_state,
next_state: self.send(state),
name: name,
user_id: self.user_id
)
end
end
def coupon_code=(code)
@coupon_code = code.strip.downcase rescue nil
end
# Tells us if there if the specified promotion is already associated with the order
# regardless of whether or not its currently eligible. Useful because generally
# you would only want a promotion action to apply to order no more than once.
#
# Receives an adjustment +originator+ (here a PromotionAction object) and tells
# if the order has adjustments from that already
def promotion_credit_exists?(originator)
!! adjustments.includes(:originator).promotion.reload.detect { |credit| credit.originator.id == originator.id }
end
def promo_total
adjustments.eligible.promotion.map(&:amount).sum
end
def shipped?
%w(partial shipped).include?(shipment_state)
end
def create_proposed_shipments
adjustments.shipping.delete_all
shipments.destroy_all
packages = Spree::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
if shipments.any?
self.shipments.destroy_all
self.update_column(:state, "address")
end
end
def refresh_shipment_rates
shipments.map &:refresh_rates
end
private
def link_by_email
self.email = user.email if self.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? or state == 'cart'
end
def ensure_line_items_present
unless line_items.present?
errors.add(:base, Spree.t(:there_are_no_items_for_this_order)) and return false
end
end
def has_available_shipment
return unless has_step?("delivery")
return unless address?
return unless ship_address && ship_address.valid?
# errors.add(:base, :no_shipping_methods_available) if available_shipping_methods.empty?
end
def ensure_available_shipping_rates
if shipments.empty? || shipments.any? { |shipment| shipment.shipping_rates.blank? }
errors.add(:base, Spree.t(:items_cannot_be_shipped)) and return false
end
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 { |shipment| shipment.cancel! }
OrderMailer.cancel_email(self.id).deliver
self.payment_state = 'credit_owed' unless shipped?
end
def after_resume
shipments.each { |shipment| shipment.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
end
end

View File

@@ -0,0 +1,67 @@
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

@@ -0,0 +1,106 @@
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) unless shipment
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 |shipment|
break if quantity == 0
quantity -= remove_from_shipment(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)
shipment = order.shipments.detect do |shipment|
(shipment.ready? || shipment.pending?) && shipment.include?(variant)
end
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,103 @@
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.size > 0 && !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
errors.add(:order, Spree.t(:has_no_shipped_units)) if order.nil? || !order.shipped_shipments.any?
end
def generate_number
return if number
record = true
while record
random = "RMA#{Array.new(9){rand(9)}.join}"
record = self.class.where(number: random).first
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,15 @@
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,6 @@
module Spree
class TokenizedPermission < ActiveRecord::Base
belongs_to :permissable, polymorphic: true
end
end

View File

@@ -0,0 +1,85 @@
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
shipment.stub(: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 #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
Spree::InventoryUnit.backordered_for_stock_item(stock_item).should =~ [unit]
end
it "does not find inventory units that aren't backordered" do
on_hand_unit = shipment.inventory_units.build
on_hand_unit.state = 'on_hand'
on_hand_unit.variant_id = 1
on_hand_unit.save!
Spree::InventoryUnit.backordered_for_stock_item(stock_item).should_not include(on_hand_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!
Spree::InventoryUnit.backordered_for_stock_item(stock_item).should_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
it "can still fetch variants by eager loading (remove default_scope)" do
unit.variant.destroy
expect(Spree::InventoryUnit.joins(:variant).includes(:variant).first.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)
inventory_units.any?(&:pending).should be_false
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
line_item.order.should_receive(:create_tax_charge!)
line_item.order.should_receive(:update!)
line_item.quantity = 2
line_item.save
end
end
context '#destroy' do
# Regression test for Spree #1481
it "applies tax adjustments" do
line_item.order.should_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
line_item.price.should == variant.price
line_item.cost_price.should == variant.cost_price
line_item.currency.should == 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
line_item.tax_category.should == 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
line_item.money.to_s.should == "$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
line_item.single_money.to_s.should == "$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).to have(0).errors_on(:quantity)
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).to have(1).errors_on(:quantity)
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).to have(0).errors_on(:quantity)
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).to have(1).errors_on(:quantity)
end
end
end
describe "scopes" do
let(:o) { create(:order) }

View File

@@ -0,0 +1,50 @@
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 = stub_model(Spree::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?
order.ship_address.should == 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?
order.ship_address.should == 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?
order.ship_address.should == 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?
order.ship_address.should be_nil
end
end
end
end
end

View File

@@ -0,0 +1,148 @@
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
order.stub(:adjustments => adjustment)
adjustment.should_receive(:destroy_all)
order.clear_adjustments!
end
it "destroy all line item adjustments" do
order.stub(:line_item_adjustments => adjustment)
adjustment.should_receive(:destroy_all)
order.clear_adjustments!
end
end
context "totaling adjustments" do
let(:adjustment1) { mock_model(Spree::Adjustment, :amount => 5) }
let(:adjustment2) { mock_model(Spree::Adjustment, :amount => 10) }
context "#ship_total" do
it "should return the correct amount" do
order.stub_chain :adjustments, :shipping => [adjustment1, adjustment2]
order.ship_total.should == 15
end
end
context "#tax_total" do
it "should return the correct amount" do
order.stub_chain :adjustments, :tax => [adjustment1, adjustment2]
order.tax_total.should == 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 { @order.stub_chain(:line_item_adjustments, :eligible => []) }
it "should return an empty hash" do
@order.line_item_adjustment_totals.should == {}
end
end
context "when there are two adjustments with different labels" do
let(:adj1) { mock_model Spree::Adjustment, :amount => 10, :label => "Foo" }
let(:adj2) { mock_model Spree::Adjustment, :amount => 20, :label => "Bar" }
before do
@order.stub_chain(:line_item_adjustments, :eligible => [adj1, adj2])
end
it "should return exactly two totals" do
@order.line_item_adjustment_totals.size.should == 2
end
it "should return the correct totals" do
@order.line_item_adjustment_totals["Foo"].should == Spree::Money.new(10)
@order.line_item_adjustment_totals["Bar"].should == Spree::Money.new(20)
end
end
context "when there are two adjustments with one label and a single adjustment with another" do
let(:adj1) { mock_model Spree::Adjustment, :amount => 10, :label => "Foo" }
let(:adj2) { mock_model Spree::Adjustment, :amount => 20, :label => "Bar" }
let(:adj3) { mock_model Spree::Adjustment, :amount => 40, :label => "Bar" }
before do
@order.stub_chain(:line_item_adjustments, :eligible => [adj1, adj2, adj3])
end
it "should return exactly two totals" do
@order.line_item_adjustment_totals.size.should == 2
end
it "should return the correct totals" do
@order.line_item_adjustment_totals["Foo"].should == Spree::Money.new(10)
@order.line_item_adjustment_totals["Bar"].should == Spree::Money.new(60)
end
end
end
context "line item adjustments" do
before do
@order = Spree::Order.create!
@order.stub :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
@order.line_item_adjustments.should 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
@order.line_item_adjustments.should =~ [@adj1, @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,42 @@
require 'spec_helper'
describe Spree::Order do
let(:order) { stub_model(Spree::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"
order.should have(:no).error_on(:email)
end
end
end
context "#save" do
context "when associated with a registered user" do
let(:user) { double(:user, :email => "test@example.com") }
before do
order.stub :user => user
end
it "should assign the email address of the user" do
order.run_callbacks(:create)
order.email.should == user.email
end
end
end
context "in the cart state" do
it "should not validate email address" do
order.state = "cart"
order.email = nil
order.should have(:no).error_on(:email)
end
end
end

View File

@@ -147,7 +147,7 @@ describe Spree::Order 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
@@ -211,7 +211,7 @@ describe Spree::Order 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,54 @@
require 'spec_helper'
module Spree
describe Spree::Order do
let(:order) { stub_model(Spree::Order) }
let(:updater) { Spree::OrderUpdater.new(order) }
before do
# So that Payment#purchase! is called during processing
Spree::Config[:auto_capture] = true
order.stub_chain(:line_items, :empty?).and_return(false)
order.stub :total => 100
end
it 'processes all payments' do
payment_1 = create(:payment, :amount => 50)
payment_2 = create(:payment, :amount => 50)
order.stub(:pending_payments).and_return([payment_1, payment_2])
order.process_payments!
updater.update_payment_state
order.payment_state.should == 'paid'
payment_1.should be_completed
payment_2.should be_completed
end
it 'does not go over total for order' do
payment_1 = create(:payment, :amount => 50)
payment_2 = create(:payment, :amount => 50)
payment_3 = create(:payment, :amount => 50)
order.stub(:pending_payments).and_return([payment_1, payment_2, payment_3])
order.process_payments!
updater.update_payment_state
order.payment_state.should == 'paid'
payment_1.should be_completed
payment_2.should be_completed
payment_3.should be_checkout
end
it "does not use failed payments" do
payment_1 = create(:payment, :amount => 50)
payment_2 = create(:payment, :amount => 50, :state => 'failed')
order.stub(:pending_payments).and_return([payment_1])
payment_2.should_not_receive(:process!)
order.process_payments!
end
end
end

View File

@@ -0,0 +1,183 @@
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
order.stub(:require_email)
end
context "#next!" do
context "when current state is confirm" do
before do
order.state = "confirm"
order.run_callbacks(:create)
order.stub :payment_required? => true
order.stub :process_payments! => true
order.stub :has_available_shipment
end
context "when payment processing succeeds" do
before { order.stub :process_payments! => true }
it "should finalize order when transitioning to complete state" do
order.should_receive(:finalize!)
order.next!
end
context "when credit card processing fails" do
before { order.stub :process_payments! => false }
it "should not complete the order" do
order.next
order.state.should == "confirm"
end
end
end
context "when payment processing fails" do
before { order.stub :process_payments! => false }
it "cannot transition to complete" do
order.next
order.state.should == "confirm"
end
end
end
context "when current state is address" do
before do
order.stub(:has_available_payment)
order.stub(: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
Spree::TaxRate.should_receive(:adjust).twice
order.next!
end
end
context "when current state is delivery" do
before do
order.state = "delivery"
order.stub :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
order.stub :completed? => true
order.shipment_state = shipment_state
order.can_cancel?.should be_true
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
order.stub :completed? => true
order.shipment_state = shipment_state
order.can_cancel?.should be_false
end
end
end
context "#cancel" do
let!(:variant) { stub_model(Spree::Variant) }
let!(:inventory_units) { [stub_model(Spree::InventoryUnit, :variant => variant),
stub_model(Spree::InventoryUnit, :variant => variant) ]}
let!(:shipment) do
shipment = stub_model(Spree::Shipment)
shipment.stub :inventory_units => inventory_units
order.stub :shipments => [shipment]
shipment
end
before do
order.stub :line_items => [stub_model(Spree::LineItem, :variant => variant, :quantity => 2)]
order.line_items.stub :find_by_variant_id => order.line_items.first
order.stub :completed? => true
order.stub :allow_cancel? => true
end
it "should send a cancel email" do
# Stub methods that cause side-effects in this test
shipment.stub(:cancel!)
order.stub :has_available_shipment
order.stub :restock_items!
mail_message = double "Mail::Message"
order_id = nil
Spree::OrderMailer.should_receive(:cancel_email) { |*args|
order_id = args[0]
mail_message
}
mail_message.should_receive :deliver
order.cancel!
order_id.should == order.id
end
context "restocking inventory" do
before do
shipment.stub(:ensure_correct_adjustment)
shipment.stub(:update_order)
Spree::OrderMailer.stub(:cancel_email).and_return(mail_message = double)
mail_message.stub :deliver
order.stub :has_available_shipment
end
end
context "resets payment state" do
before do
# Stubs methods that cause unwanted side effects in this test
Spree::OrderMailer.stub(:cancel_email).and_return(mail_message = double)
mail_message.stub :deliver
order.stub :has_available_shipment
order.stub :restock_items!
shipment.stub(:cancel!)
end
context "without shipped items" do
it "should set payment state to 'credit owed'" do
order.cancel!
order.payment_state.should == 'credit_owed'
end
end
context "with shipped items" do
before do
order.stub :shipment_state => 'partial'
end
it "should not alter the payment state" do
order.cancel!
order.payment_state.should be_nil
end
end
end
end
# Another regression test for Spree #729
context "#resume" do
before do
order.stub :email => "user@spreecommerce.com"
order.stub :state => "canceled"
order.stub :allow_resume? => true
# Stubs method that cause unwanted side effects in this test
order.stub :has_available_shipment
end
end
end

View File

@@ -0,0 +1,115 @@
require 'spec_helper'
module Spree
describe Spree::Order do
let(:order) { stub_model(Spree::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
order.tax_zone.should 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
Spree::Zone.should_receive(:match).at_least(:once).with(ship_address)
Spree::Zone.should_not_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
Spree::Zone.should_receive(:match).at_least(:once).with(bill_address)
Spree::Zone.should_not_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")
Spree::Zone.stub :default_tax => @default_zone
end
context "when there is a matching zone" do
before { Spree::Zone.stub(:match => zone) }
it "should return the matching zone" do
order.tax_zone.should == zone
end
end
context "when there is no matching zone" do
before { Spree::Zone.stub(:match => nil) }
it "should return the default tax zone" do
order.tax_zone.should == @default_zone
end
end
end
context "when no default tax zone" do
before { Spree::Zone.stub :default_tax => nil }
context "when there is a matching zone" do
before { Spree::Zone.stub(:match => zone) }
it "should return the matching zone" do
order.tax_zone.should == zone
end
end
context "when there is no matching zone" do
before { Spree::Zone.stub(:match => nil) }
it "should return nil" do
order.tax_zone.should be_nil
end
end
end
end
context "#exclude_tax?" do
before do
@order = create(:order)
@default_zone = create(:zone)
Spree::Zone.stub :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
@order.stub :tax_zone => create(:zone, :name => "other_zone")
@order.exclude_tax?.should be_true
end
it "should be false when tax_zone is the same as the default" do
@order.stub :tax_zone => @default_zone
@order.exclude_tax?.should be_false
end
end
context "when prices do not include tax" do
before { Spree::Config.set(:prices_inc_tax => false) }
it "should be false" do
@order.exclude_tax?.should be_false
end
end
end
end
end

View File

@@ -0,0 +1,18 @@
require 'spec_helper'
describe Spree::Order do
let(:order) { stub_model(Spree::Order) }
context "#update!" do
let(:line_items) { [mock_model(Spree::LineItem, :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
order.should_receive :foo
order.update!
end
end
end
end

View File

@@ -0,0 +1,92 @@
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)
line_item.quantity.should == 1
order.line_items.size.should == 1
end
end
it 'should add line item if one does not exist' do
line_item = subject.add(variant, 1)
line_item.quantity.should == 1
order.line_items.size.should == 1
end
it 'should update line item if one exists' do
subject.add(variant, 1)
line_item = subject.add(variant, 1)
line_item.quantity.should == 2
order.line_items.size.should == 1
end
it "should update order totals" do
order.item_total.to_f.should == 0.00
order.total.to_f.should == 0.00
subject.add(variant, 1)
order.item_total.to_f.should == 19.99
order.total.to_f.should == 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)
line_item.reload.quantity.should == 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)
line_item.reload.quantity.should == 2
end
it 'should remove line_item if quantity matches line_item quantity' do
subject.add(variant, 1)
subject.remove(variant, 1)
order.reload.find_line_item_by_variant(variant).should be_nil
end
it "should update order totals" do
order.item_total.to_f.should == 0.00
order.total.to_f.should == 0.00
subject.add(variant,2)
order.item_total.to_f.should == 39.98
order.total.to_f.should == 39.98
subject.remove(variant,1)
order.item_total.to_f.should == 19.99
order.total.to_f.should == 19.99
end
end
end

View File

@@ -0,0 +1,174 @@
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)
units.map(&:variant_id).should == [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
order.shipments.first.inventory_units_for(line_item.variant).size.should == 1
line_item.reload.quantity.should == 2
end
it 'should increase the number of inventory units' do
subject.verify(line_item)
order.reload.shipments.first.inventory_units_for(line_item.variant).size.should == 2
end
end
context "#add_to_shipment" do
let(:shipment) { order.shipments.first }
let(:variant) { create :variant }
context "order is not completed" do
before { order.stub completed?: false }
it "doesn't unstock items" do
shipment.stock_location.should_not_receive(:unstock)
subject.send(:add_to_shipment, shipment, variant, 5).should == 5
end
end
it 'should create inventory_units in the necessary states' do
shipment.stock_location.should_receive(:fill_status).with(variant, 5).and_return([3, 2])
subject.send(:add_to_shipment, shipment, variant, 5).should == 5
units = shipment.inventory_units.group_by &:variant_id
units = units[variant.id].group_by &:state
units['backordered'].size.should == 2
units['on_hand'].size.should == 3
end
it 'should create stock_movement' do
subject.send(:add_to_shipment, shipment, variant, 5).should == 5
stock_item = shipment.stock_location.stock_item(variant)
movement = stock_item.stock_movements.last
# movement.originator.should == shipment
movement.quantity.should == -5
end
end
context "#determine_target_shipment" do
let(:stock_location) { create :stock_location }
let(:variant) { line_item.variant }
before do
order.shipments.create(:stock_location_id => stock_location.id)
shipped = order.shipments.create(:stock_location_id => order.shipments.first.stock_location.id)
shipped.update_column(:state, 'shipped')
end
it 'should select first non-shipped shipment that already contains given variant' do
shipment = subject.send(:determine_target_shipment, variant)
shipment.shipped?.should be_false
shipment.inventory_units_for(variant).should_not be_empty
variant.stock_location_ids.include?(shipment.stock_location_id).should be_true
end
context "when no shipments already contain this varint" do
it 'selects first non-shipped shipment that leaves from same stock_location' do
subject.send(:remove_from_shipment, order.shipments.first, variant, line_item.quantity)
shipment = subject.send(:determine_target_shipment, variant)
shipment.reload
shipment.shipped?.should be_false
shipment.inventory_units_for(variant).should be_empty
variant.stock_location_ids.include?(shipment.stock_location_id).should be_true
end
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
order.shipments.first.inventory_units_for(line_item.variant).size.should == 3
line_item.quantity.should == 2
end
it 'should decrease the number of inventory units' do
subject.verify(line_item)
order.reload.shipments.first.inventory_units_for(line_item.variant).size.should == 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 { order.stub completed?: false }
it "doesn't restock items" do
shipment.stock_location.should_not_receive(:restock)
subject.send(:remove_from_shipment, shipment, variant, 1).should == 1
end
end
it 'should create stock_movement' do
subject.send(:remove_from_shipment, shipment, variant, 1).should == 1
stock_item = shipment.stock_location.stock_item(variant)
movement = stock_item.stock_movements.last
# movement.originator.should == shipment
movement.quantity.should == 1
end
it 'should destroy backordered units first' do
shipment.stub(:inventory_units_for => [ mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'backordered'),
mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'on_hand'),
mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'backordered') ])
shipment.inventory_units_for[0].should_receive(:destroy)
shipment.inventory_units_for[1].should_not_receive(:destroy)
shipment.inventory_units_for[2].should_receive(:destroy)
subject.send(:remove_from_shipment, shipment, variant, 2).should == 2
end
it 'should destroy unshipped units first' do
shipment.stub(:inventory_units_for => [ mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'shipped'),
mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'on_hand') ] )
shipment.inventory_units_for[0].should_not_receive(:destroy)
shipment.inventory_units_for[1].should_receive(:destroy)
subject.send(:remove_from_shipment, shipment, variant, 1).should == 1
end
it 'only attempts to destroy as many units as are eligible, and return amount destroyed' do
shipment.stub(:inventory_units_for => [ mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'shipped'),
mock_model(Spree::InventoryUnit, :variant_id => variant.id, :state => 'on_hand') ] )
shipment.inventory_units_for[0].should_not_receive(:destroy)
shipment.inventory_units_for[1].should_receive(:destroy)
subject.send(:remove_from_shipment, shipment, variant, 1).should == 1
end
it 'should destroy self if not inventory units remain' do
shipment.inventory_units.stub(:count => 0)
shipment.should_receive(:destroy)
subject.send(:remove_from_shipment, shipment, variant, 1).should == 1
end
end
end
end

View File

@@ -3,6 +3,617 @@ require 'spec_helper'
describe Spree::Order do
include OpenFoodNetwork::EmailHelper
let(:user) { stub_model(Spree::LegacyUser, :email => "spree@example.com") }
let(:order) { stub_model(Spree::Order, :user => user) }
before do
Spree::LegacyUser.stub(:current => mock_model(Spree::LegacyUser, :id => 123))
end
context "#products" do
before :each do
@variant1 = mock_model(Spree::Variant, :product => "product1")
@variant2 = mock_model(Spree::Variant, :product => "product2")
@line_items = [mock_model(Spree::LineItem, :product => "product1", :variant => @variant1, :variant_id => @variant1.id, :quantity => 1),
mock_model(Spree::LineItem, :product => "product2", :variant => @variant2, :variant_id => @variant2.id, :quantity => 2)]
order.stub(:line_items => @line_items)
end
it "should return ordered products" do
order.products.should == ['product1', 'product2']
end
it "contains?" do
order.contains?(@variant1).should be_true
end
it "gets the quantity of a given variant" do
order.quantity_of(@variant1).should == 1
@variant3 = mock_model(Spree::Variant, :product => "product3")
order.quantity_of(@variant3).should == 0
end
it "can find a line item matching a given variant" do
order.find_line_item_by_variant(@variant1).should_not be_nil
order.find_line_item_by_variant(mock_model(Spree::Variant)).should be_nil
end
end
context "#generate_order_number" do
it "should generate a random string" do
order.generate_order_number.is_a?(String).should be_true
(order.generate_order_number.to_s.length > 0).should be_true
end
end
context "#associate_user!" do
it "should associate a user with a persisted order" do
order = FactoryGirl.create(:order_with_line_items, created_by: nil)
user = FactoryGirl.create(:user)
order.user = nil
order.email = nil
order.associate_user!(user)
order.user.should == user
order.email.should == user.email
order.created_by.should == user
# verify that the changes we made were persisted
order.reload
order.user.should == user
order.email.should == user.email
order.created_by.should == user
end
it "should not overwrite the created_by if it already is set" do
creator = create(:user)
order = FactoryGirl.create(:order_with_line_items, created_by: creator)
user = FactoryGirl.create(:user)
order.user = nil
order.email = nil
order.associate_user!(user)
order.user.should == user
order.email.should == user.email
order.created_by.should == creator
# verify that the changes we made were persisted
order.reload
order.user.should == user
order.email.should == user.email
order.created_by.should == 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
order.number.should_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
order.stub(:complete? => true)
order.can_ship?.should be_true
end
it "should be true for order in the 'resumed' state" do
order.stub(:resumed? => true)
order.can_ship?.should be_true
end
it "should be true for an order in the 'awaiting return' state" do
order.stub(:awaiting_return? => true)
order.can_ship?.should be_true
end
it "should be true for an order in the 'returned' state" do
order.stub(:returned? => true)
order.can_ship?.should be_true
end
it "should be false if the order is neither in the 'complete' nor 'resumed' state" do
order.stub(:resumed? => false, :complete? => false)
order.can_ship?.should be_false
end
end
context "checking if order is paid" do
context "payment_state is paid" do
before { order.stub payment_state: 'paid' }
it { expect(order).to be_paid }
end
context "payment_state is credit_owned" do
before { order.stub 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
order.should_receive(:touch).with(:completed_at)
order.finalize!
end
it "should sell inventory units" do
order.shipments.each do |shipment|
shipment.should_receive(:update!)
shipment.should_receive(:finalize!)
end
order.finalize!
end
it "should decrease the stock for each variant in the shipment" do
order.shipments.each do |shipment|
shipment.stock_location.should_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
order.stub(:paid? => true, :complete? => true)
order.finalize!
order.reload # reload so we're sure the changes are persisted
order.shipment_state.should == 'ready'
end
after { Spree::Config.set :track_inventory_levels => true }
it "should not sell inventory units if track_inventory_levels is false" do
Spree::Config.set :track_inventory_levels => false
Spree::InventoryUnit.should_not_receive(:sell_units)
order.finalize!
end
it "should send an order confirmation email" do
mail_message = double "Mail::Message"
Spree::OrderMailer.should_receive(:confirm_email).with(order.id).and_return mail_message
mail_message.should_receive :deliver
order.finalize!
end
it "should continue even if confirmation email delivery fails" do
Spree::OrderMailer.should_receive(:confirm_email).with(order.id).and_raise 'send failed!'
order.finalize!
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
order.stub :has_available_shipment
Spree::OrderMailer.stub_chain :confirm_email, :deliver
adjustments = double
order.stub :adjustments => adjustments
expect(adjustments).to receive(:update_all).with(state: 'closed')
order.finalize!
end
it "should log state event" do
order.state_changes.should_receive(:create).exactly(3).times #order, shipment & payment state changes
order.finalize!
end
it 'calls updater#before_save' do
order.updater.should_receive(:before_save_hook)
order.finalize!
end
end
context "#process_payments!" do
let(:payment) { stub_model(Spree::Payment) }
before { order.stub :pending_payments => [payment], :total => 10 }
it "should process the payments" do
payment.should_receive(:process!)
order.process_payments!.should be_true
end
it "should return false if no pending_payments available" do
order.stub :pending_payments => []
order.process_payments!.should be_false
end
context "when a payment raises a GatewayError" do
before { payment.should_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
order.process_payments!.should be_true
end
it "should return false when not configured to allow checkout on gateway failures" do
Spree::Config.set :allow_checkout_on_gateway_error => false
order.process_payments!.should be_false
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
order.outstanding_balance.should == 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
order.outstanding_balance.should 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
order.outstanding_balance?.should be_true
end
it "should be true when total less than payment_total" do
order.total = 8.25
order.payment_total = 10.44
order.outstanding_balance?.should be_true
end
it "should be false when total equals payment_total" do
order.total = 10.10
order.payment_total = 10.10
order.outstanding_balance?.should be_false
end
end
context "#completed?" do
it "should indicate if order is completed" do
order.completed_at = nil
order.completed?.should be_false
order.completed_at = Time.now
order.completed?.should be_true
end
end
it 'is backordered if one of the shipments is backordered' do
order.stub(:shipments => [mock_model(Spree::Shipment, :backordered? => false),
mock_model(Spree::Shipment, :backordered? => true)])
order.should be_backordered
end
context "#allow_checkout?" do
it "should be true if there are line_items in the order" do
order.stub_chain(:line_items, :count => 1)
order.checkout_allowed?.should be_true
end
it "should be false if there are no line_items in the order" do
order.stub_chain(:line_items, :count => 0)
order.checkout_allowed?.should be_false
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
@order.item_count.should == 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
@order.amount.should == 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.now
order.can_cancel?.should be_false
end
it "should be true for completed order with no shipment" do
order.state = 'complete'
order.shipment_state = nil
order.completed_at = Time.now
order.can_cancel?.should be_true
end
end
context "insufficient_stock_lines" do
let(:line_item) { mock_model Spree::LineItem, :insufficient_stock? => true }
before { order.stub(:line_items => [line_item]) }
it "should return line_item that has insufficient stock on hand" do
order.insufficient_stock_lines.size.should == 1
order.insufficient_stock_lines.include?(line_item).should be_true
end
end
context "empty!" do
it "should clear out all line items and adjustments" do
order = stub_model(Spree::Order)
order.stub(:line_items => line_items = [])
order.stub(:adjustments => adjustments = [])
order.line_items.should_receive(:destroy_all)
order.adjustments.should_receive(:destroy_all)
order.empty!
end
end
context "#display_outstanding_balance" do
it "returns the value as a spree money" do
order.stub(:outstanding_balance) { 10.55 }
order.display_outstanding_balance.should == Spree::Money.new(10.55)
end
end
context "#display_item_total" do
it "returns the value as a spree money" do
order.stub(:item_total) { 10.55 }
order.display_item_total.should == 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
order.display_adjustment_total.should == Spree::Money.new(10.55)
end
end
context "#display_total" do
it "returns the value as a spree money" do
order.total = 10.55
order.display_total.should == 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
order.currency.should == "ABC"
end
end
context "when object currency is nil" do
before { order.currency = nil }
it "returns the globally configured currency" do
order.currency.should == "USD"
end
end
end
# Regression tests for Spree #2179
context "#merge!" do
let(:variant) { create(:variant) }
let(:order_1) { Spree::Order.create }
let(:order_2) { Spree::Order.create }
it "destroys the other order" do
order_1.merge!(order_2)
lambda { order_2.reload }.should raise_error(ActiveRecord::RecordNotFound)
end
context "merging together two orders with line items for the same variant" do
before do
order_1.contents.add(variant, 1)
order_2.contents.add(variant, 1)
end
specify do
order_1.merge!(order_2)
order_1.line_items.count.should == 1
line_item = order_1.line_items.first
line_item.quantity.should == 2
line_item.variant_id.should == variant.id
end
end
context "merging together two orders with different line items" do
let(:variant_2) { create(:variant) }
before do
order_1.contents.add(variant, 1)
order_2.contents.add(variant_2, 1)
end
specify do
order_1.merge!(order_2)
line_items = order_1.line_items
line_items.count.should == 2
# No guarantee on ordering of line items, so we do this:
line_items.pluck(:quantity).should =~ [1, 1]
line_items.pluck(:variant_id).should =~ [variant.id, variant_2.id]
end
end
end
context "#confirmation_required?" do
it "does not bomb out when an order has an unpersisted payment" do
order = Spree::Order.new
order.payments.build
assert !order.confirmation_required?
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
persisted_order.stub(: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
persisted_order.stub(payment_required?: true)
persisted_order.next!
persisted_order.state.should == "payment"
end
end
context "promotion adjustments" do
let(:originator) { double("Originator", id: 1) }
let(:adjustment) { double("Adjustment", originator: originator) }
before { order.stub_chain(:adjustments, :includes, :promotion, reload: [adjustment]) }
context "order has an adjustment from given promo action" do
it { expect(order.promotion_credit_exists? originator).to be_true }
end
context "order has no adjustment from given promo action" do
before { originator.stub(id: 12) }
it { expect(order.promotion_credit_exists? originator).to be_true }
end
end
context "payment required?" do
let(:order) { Spree::Order.new }
context "total is zero" do
it { order.payment_required?.should be_false }
end
context "total > zero" do
before { order.stub(total: 1) }
it { order.payment_required?.should be_true }
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)
order.should_receive(:add_awesome_sauce)
order.update!
end
it "calls hook during finalize" do
order = create(:order)
order.should_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
subject.should == 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
subject.should == order.bill_address
end
end
end
context '#updater' do
class FakeOrderUpdaterDecorator
attr_reader :decorated_object
def initialize(decorated_object)
@decorated_object = decorated_object
end
end
before do
Spree::Config.stub(:order_updater_decorator) { FakeOrderUpdaterDecorator }
end
it 'returns an order_updater_decorator class' do
order.updater.class.should == FakeOrderUpdaterDecorator
end
it 'decorates a Spree::OrderUpdater' do
order.updater.decorated_object.class.should == Spree::OrderUpdater
end
end
describe "email validation" do
let(:order) { build(:order) }

View File

@@ -0,0 +1,138 @@
require 'spec_helper'
describe Spree::ReturnAuthorization do
let(:stock_location) {Spree::StockLocation.create(:name => "test")}
let(:order) { FactoryGirl.create(:shipped_order)}
let(:variant) { order.shipments.first.inventory_units.first.variant }
let(:return_authorization) { Spree::ReturnAuthorization.new(:order => order, :stock_location_id => stock_location.id) }
context "save" do
it "should be invalid when order has no inventory units" do
order.shipments.destroy_all
return_authorization.save
return_authorization.errors[:order].should == ["has no shipped units"]
end
it "should generate RMA number" do
return_authorization.should_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)
return_authorization.inventory_units.size.should == 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
order.should_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
return_authorization.stub(:inventory_units => [1,2,3])
return_authorization.can_receive?.should be_true
end
it "should not allow_receive with no inventory units" do
return_authorization.stub(:inventory_units => [])
return_authorization.can_receive?.should be_false
end
end
context "receive!" do
let(:inventory_unit) { order.shipments.first.inventory_units.first }
before do
return_authorization.stub(:inventory_units => [inventory_unit], :amount => -20)
Spree::Adjustment.stub(:create)
order.stub(:update!)
end
it "should mark all inventory units are returned" do
inventory_unit.should_receive(:return!)
return_authorization.receive!
end
it "should add credit for specified amount" do
return_authorization.amount = 20
mock_adjustment = double
mock_adjustment.should_receive(:source=).with(return_authorization)
mock_adjustment.should_receive(:adjustable=).with(order)
mock_adjustment.should_receive(:save)
Spree::Adjustment.should_receive(:new).with(:amount => -20, :label => Spree.t(:rma_credit)).and_return(mock_adjustment)
return_authorization.receive!
end
it "should update order state" do
order.should_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
return_authorization.amount.should == 10
end
end
context "after_save" do
it "should run correct callbacks" do
return_authorization.should_receive(:force_positive_amount)
return_authorization.run_callbacks(:save)
end
end
context "currency" do
before { order.stub(:currency) { "ABC" } }
it "returns the order currency" do
return_authorization.currency.should == "ABC"
end
end
context "display_amount" do
it "returns a Spree::Money" do
return_authorization.amount = 21.22
return_authorization.display_amount.should == Spree::Money.new(21.22)
end
end
context "returnable_inventory" do
pending "should return inventory from shipped shipments" do
return_authorization.returnable_inventory.should == [inventory_unit]
end
pending "should not return inventory from unshipped shipments" do
return_authorization.returnable_inventory.should == []
end
end
end