mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Bring models related to Order from spree_core
EPIC COMMIT ALERT :-)
This commit is contained in:
70
app/models/spree/inventory_unit.rb
Normal file
70
app/models/spree/inventory_unit.rb
Normal 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
|
||||
|
||||
102
app/models/spree/line_item.rb
Normal file
102
app/models/spree/line_item.rb
Normal 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
575
app/models/spree/order.rb
Normal 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
|
||||
67
app/models/spree/order_contents.rb
Normal file
67
app/models/spree/order_contents.rb
Normal 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
|
||||
106
app/models/spree/order_inventory.rb
Normal file
106
app/models/spree/order_inventory.rb
Normal 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
|
||||
103
app/models/spree/return_authorization.rb
Normal file
103
app/models/spree/return_authorization.rb
Normal 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
|
||||
15
app/models/spree/state_change.rb
Normal file
15
app/models/spree/state_change.rb
Normal 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
|
||||
6
app/models/spree/tokenized_permission.rb
Normal file
6
app/models/spree/tokenized_permission.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
module Spree
|
||||
class TokenizedPermission < ActiveRecord::Base
|
||||
belongs_to :permissable, polymorphic: true
|
||||
end
|
||||
end
|
||||
|
||||
85
spec/models/spree/inventory_unit_spec.rb
Normal file
85
spec/models/spree/inventory_unit_spec.rb
Normal 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
|
||||
@@ -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) }
|
||||
|
||||
|
||||
50
spec/models/spree/order/address_spec.rb
Normal file
50
spec/models/spree/order/address_spec.rb
Normal 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
|
||||
148
spec/models/spree/order/adjustments_spec.rb
Normal file
148
spec/models/spree/order/adjustments_spec.rb
Normal 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
|
||||
|
||||
42
spec/models/spree/order/callbacks_spec.rb
Normal file
42
spec/models/spree/order/callbacks_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
54
spec/models/spree/order/payment_spec.rb
Normal file
54
spec/models/spree/order/payment_spec.rb
Normal 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
|
||||
183
spec/models/spree/order/state_machine_spec.rb
Normal file
183
spec/models/spree/order/state_machine_spec.rb
Normal 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
|
||||
115
spec/models/spree/order/tax_spec.rb
Normal file
115
spec/models/spree/order/tax_spec.rb
Normal 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
|
||||
18
spec/models/spree/order/updating_spec.rb
Normal file
18
spec/models/spree/order/updating_spec.rb
Normal 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
|
||||
92
spec/models/spree/order_contents_spec.rb
Normal file
92
spec/models/spree/order_contents_spec.rb
Normal 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
|
||||
174
spec/models/spree/order_inventory_spec.rb
Normal file
174
spec/models/spree/order_inventory_spec.rb
Normal 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
|
||||
@@ -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) }
|
||||
|
||||
|
||||
138
spec/models/spree/return_authorization_spec.rb
Normal file
138
spec/models/spree/return_authorization_spec.rb
Normal 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
|
||||
Reference in New Issue
Block a user