mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-28 01:53:25 +00:00
Merge pull request #2542 from luisramos0/add_to_cart
Extract OrderController.populate to new CartController and OrderPopulator to new CartService
This commit is contained in:
@@ -41,7 +41,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $roo
|
||||
update: =>
|
||||
@update_running = true
|
||||
|
||||
$http.post('/orders/populate', @data()).success (data, status)=>
|
||||
$http.post('/cart/populate', @data()).success (data, status)=>
|
||||
@saved()
|
||||
@update_running = false
|
||||
|
||||
|
||||
99
app/controllers/cart_controller.rb
Normal file
99
app/controllers/cart_controller.rb
Normal file
@@ -0,0 +1,99 @@
|
||||
require 'spree/core/controller_helpers/order_decorator'
|
||||
|
||||
class CartController < BaseController
|
||||
before_filter :check_authorization
|
||||
|
||||
def populate
|
||||
# Without intervention, the Spree::Adjustment#update_adjustable callback is called many times
|
||||
# during cart population, for both taxation and enterprise fees. This operation triggers a
|
||||
# costly Spree::Order#update!, which only needs to be run once. We avoid this by disabling
|
||||
# callbacks on Spree::Adjustment and then manually invoke Spree::Order#update! on success.
|
||||
Spree::Adjustment.without_callbacks do
|
||||
cart_service = CartService.new(current_order(true))
|
||||
|
||||
if cart_service.populate(params.slice(:products, :variants, :quantity), true)
|
||||
fire_event('spree.cart.add')
|
||||
fire_event('spree.order.contents_changed')
|
||||
|
||||
current_order.cap_quantity_at_stock!
|
||||
current_order.update!
|
||||
|
||||
variant_ids = variant_ids_in(cart_service.variants_h)
|
||||
|
||||
render json: { error: false, stock_levels: stock_levels(current_order, variant_ids) },
|
||||
status: 200
|
||||
|
||||
else
|
||||
render json: { error: true }, status: 412
|
||||
end
|
||||
end
|
||||
populate_variant_attributes
|
||||
end
|
||||
|
||||
# Report the stock levels in the order for all variant ids requested
|
||||
def stock_levels(order, variant_ids)
|
||||
stock_levels = li_stock_levels(order)
|
||||
|
||||
li_variant_ids = stock_levels.keys
|
||||
(variant_ids - li_variant_ids).each do |variant_id|
|
||||
stock_levels[variant_id] = { quantity: 0, max_quantity: 0, on_hand: Spree::Variant.find(variant_id).on_hand }
|
||||
end
|
||||
|
||||
stock_levels
|
||||
end
|
||||
|
||||
def variant_ids_in(variants_h)
|
||||
variants_h.map { |v| v[:variant_id].to_i }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
session[:access_token] ||= params[:token]
|
||||
order = Spree::Order.find_by_number(params[:id]) || current_order
|
||||
|
||||
if order
|
||||
authorize! :edit, order, session[:access_token]
|
||||
else
|
||||
authorize! :create, Spree::Order
|
||||
end
|
||||
end
|
||||
|
||||
def populate_variant_attributes
|
||||
order = current_order.reload
|
||||
|
||||
populate_variant_attributes_from_variant(order) if params.key? :variant_attributes
|
||||
populate_variant_attributes_from_product(order) if params.key? :quantity
|
||||
end
|
||||
|
||||
def populate_variant_attributes_from_variant(order)
|
||||
params[:variant_attributes].each do |variant_id, attributes|
|
||||
order.set_variant_attributes(Spree::Variant.find(variant_id), attributes)
|
||||
end
|
||||
end
|
||||
|
||||
def populate_variant_attributes_from_product(order)
|
||||
params[:products].each do |_product_id, variant_id|
|
||||
max_quantity = params[:max_quantity].to_i
|
||||
order.set_variant_attributes(Spree::Variant.find(variant_id),
|
||||
max_quantity: max_quantity)
|
||||
end
|
||||
end
|
||||
|
||||
def li_stock_levels(order)
|
||||
Hash[
|
||||
order.line_items.map do |li|
|
||||
[li.variant.id,
|
||||
{ quantity: li.quantity,
|
||||
max_quantity: li.max_quantity,
|
||||
on_hand: wrap_json_infinity(li.variant.on_hand) }]
|
||||
end
|
||||
]
|
||||
end
|
||||
|
||||
# Rails to_json encodes Float::INFINITY as Infinity, which is not valid JSON
|
||||
# Return it as a large integer (max 32 bit signed int)
|
||||
def wrap_json_infinity(number)
|
||||
number == Float::INFINITY ? 2147483647 : number
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,6 @@
|
||||
require 'spree/core/controller_helpers/order_decorator'
|
||||
|
||||
Spree::OrdersController.class_eval do
|
||||
after_filter :populate_variant_attributes, only: :populate
|
||||
before_filter :update_distribution, only: :update
|
||||
before_filter :filter_order_params, only: :update
|
||||
before_filter :enable_embedded_shopfront
|
||||
@@ -71,61 +70,6 @@ Spree::OrdersController.class_eval do
|
||||
end
|
||||
end
|
||||
|
||||
def populate
|
||||
# Without intervention, the Spree::Adjustment#update_adjustable callback is called many times
|
||||
# during cart population, for both taxation and enterprise fees. This operation triggers a
|
||||
# costly Spree::Order#update!, which only needs to be run once. We avoid this by disabling
|
||||
# callbacks on Spree::Adjustment and then manually invoke Spree::Order#update! on success.
|
||||
|
||||
Spree::Adjustment.without_callbacks do
|
||||
populator = Spree::OrderPopulator.new(current_order(true), current_currency)
|
||||
|
||||
if populator.populate(params.slice(:products, :variants, :quantity), true)
|
||||
fire_event('spree.cart.add')
|
||||
fire_event('spree.order.contents_changed')
|
||||
|
||||
current_order.cap_quantity_at_stock!
|
||||
current_order.update!
|
||||
|
||||
variant_ids = variant_ids_in(populator.variants_h)
|
||||
|
||||
render json: {error: false, stock_levels: stock_levels(current_order, variant_ids)},
|
||||
status: 200
|
||||
|
||||
else
|
||||
render json: {error: true}, status: 412
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Report the stock levels in the order for all variant ids requested
|
||||
def stock_levels(order, variant_ids)
|
||||
stock_levels = li_stock_levels(order)
|
||||
|
||||
li_variant_ids = stock_levels.keys
|
||||
(variant_ids - li_variant_ids).each do |variant_id|
|
||||
stock_levels[variant_id] = {quantity: 0, max_quantity: 0,
|
||||
on_hand: Spree::Variant.find(variant_id).on_hand}
|
||||
end
|
||||
|
||||
stock_levels
|
||||
end
|
||||
|
||||
def variant_ids_in(variants_h)
|
||||
variants_h.map { |v| v[:variant_id].to_i }
|
||||
end
|
||||
|
||||
def li_stock_levels(order)
|
||||
Hash[
|
||||
order.line_items.map do |li|
|
||||
[li.variant.id,
|
||||
{quantity: li.quantity,
|
||||
max_quantity: li.max_quantity,
|
||||
on_hand: wrap_json_infinity(li.variant.on_hand)}]
|
||||
end
|
||||
]
|
||||
end
|
||||
|
||||
def update_distribution
|
||||
@order = current_order(true)
|
||||
|
||||
@@ -184,30 +128,6 @@ Spree::OrdersController.class_eval do
|
||||
|
||||
private
|
||||
|
||||
def populate_variant_attributes
|
||||
order = current_order.reload
|
||||
|
||||
if params.key? :variant_attributes
|
||||
params[:variant_attributes].each do |variant_id, attributes|
|
||||
order.set_variant_attributes(Spree::Variant.find(variant_id), attributes)
|
||||
end
|
||||
end
|
||||
|
||||
if params.key? :quantity
|
||||
params[:products].each do |product_id, variant_id|
|
||||
max_quantity = params[:max_quantity].to_i
|
||||
order.set_variant_attributes(Spree::Variant.find(variant_id),
|
||||
{:max_quantity => max_quantity})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Rails to_json encodes Float::INFINITY as Infinity, which is not valid JSON
|
||||
# Return it as a large integer (max 32 bit signed int)
|
||||
def wrap_json_infinity(n)
|
||||
n == Float::INFINITY ? 2147483647 : n
|
||||
end
|
||||
|
||||
def order_to_update
|
||||
return @order_to_update if defined? @order_to_update
|
||||
return @order_to_update = current_order unless params[:id]
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
Spree::OrderPopulator.class_eval do
|
||||
class CartService
|
||||
attr_accessor :order, :currency
|
||||
attr_reader :variants_h
|
||||
attr_reader :errors
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
@currency = order.currency
|
||||
@errors = ActiveModel::Errors.new(self)
|
||||
end
|
||||
|
||||
def populate(from_hash, overwrite = false)
|
||||
@distributor, @order_cycle = distributor_and_order_cycle
|
||||
@@ -9,46 +17,21 @@ Spree::OrderPopulator.class_eval do
|
||||
# this validation probably can't fail
|
||||
if !distribution_can_supply_products_in_cart(@distributor, @order_cycle)
|
||||
errors.add(:base, I18n.t(:spree_order_populator_error))
|
||||
return false
|
||||
end
|
||||
|
||||
if valid?
|
||||
@order.with_lock do
|
||||
variants = read_variants from_hash
|
||||
|
||||
variants.each do |v|
|
||||
if varies_from_cart(v)
|
||||
attempt_cart_add(v[:variant_id], v[:quantity], v[:max_quantity])
|
||||
end
|
||||
end
|
||||
|
||||
if overwrite
|
||||
variants_removed(variants).each do |id|
|
||||
cart_remove(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@order.with_lock do
|
||||
variants = read_variants from_hash
|
||||
attempt_cart_add_variants variants
|
||||
overwrite_variants variants unless !overwrite
|
||||
end
|
||||
|
||||
valid?
|
||||
end
|
||||
|
||||
def read_variants(data)
|
||||
@variants_h = read_products_hash(data) +
|
||||
read_variants_hash(data)
|
||||
end
|
||||
|
||||
def read_products_hash(data)
|
||||
(data[:products] || []).map do |product_id, variant_id|
|
||||
{variant_id: variant_id, quantity: data[:quantity]}
|
||||
end
|
||||
end
|
||||
|
||||
def read_variants_hash(data)
|
||||
(data[:variants] || []).map do |variant_id, quantity|
|
||||
if quantity.is_a?(Hash)
|
||||
{variant_id: variant_id, quantity: quantity[:quantity], max_quantity: quantity[:max_quantity]}
|
||||
else
|
||||
{variant_id: variant_id, quantity: quantity}
|
||||
def attempt_cart_add_variants(variants)
|
||||
variants.each do |v|
|
||||
if varies_from_cart(v)
|
||||
attempt_cart_add(v[:variant_id], v[:quantity], v[:max_quantity])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -58,17 +41,18 @@ Spree::OrderPopulator.class_eval do
|
||||
max_quantity = max_quantity.to_i if max_quantity
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
OpenFoodNetwork::ScopeVariantToHub.new(@distributor).scope(variant)
|
||||
if quantity > 0 &&
|
||||
check_order_cycle_provided_for(variant) &&
|
||||
check_variant_available_under_distribution(variant)
|
||||
|
||||
quantity_to_add, max_quantity_to_add = quantities_to_add(variant, quantity, max_quantity)
|
||||
return unless quantity > 0 && valid_variant?(variant)
|
||||
|
||||
if quantity_to_add > 0
|
||||
@order.add_variant(variant, quantity_to_add, max_quantity_to_add, currency)
|
||||
else
|
||||
@order.remove_variant variant
|
||||
end
|
||||
cart_add(variant, quantity, max_quantity)
|
||||
end
|
||||
|
||||
def cart_add(variant, quantity, max_quantity)
|
||||
quantity_to_add, max_quantity_to_add = quantities_to_add(variant, quantity, max_quantity)
|
||||
if quantity_to_add > 0
|
||||
@order.add_variant(variant, quantity_to_add, max_quantity_to_add, currency)
|
||||
else
|
||||
@order.remove_variant variant
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,14 +66,44 @@ Spree::OrderPopulator.class_eval do
|
||||
[quantity_to_add, max_quantity_to_add]
|
||||
end
|
||||
|
||||
def overwrite_variants(variants)
|
||||
variants_removed(variants).each do |id|
|
||||
cart_remove(id)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_variants(data)
|
||||
@variants_h = read_products_hash(data) +
|
||||
read_variants_hash(data)
|
||||
end
|
||||
|
||||
def read_products_hash(data)
|
||||
(data[:products] || []).map do |_product_id, variant_id|
|
||||
{ variant_id: variant_id, quantity: data[:quantity] }
|
||||
end
|
||||
end
|
||||
|
||||
def read_variants_hash(data)
|
||||
(data[:variants] || []).map do |variant_id, quantity|
|
||||
if quantity.is_a?(Hash)
|
||||
{ variant_id: variant_id, quantity: quantity[:quantity], max_quantity: quantity[:max_quantity] }
|
||||
else
|
||||
{ variant_id: variant_id, quantity: quantity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def valid?
|
||||
errors.empty?
|
||||
end
|
||||
|
||||
def cart_remove(variant_id)
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
@order.remove_variant(variant)
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def distributor_and_order_cycle
|
||||
[@order.distributor, @order.order_cycle]
|
||||
end
|
||||
@@ -114,6 +128,10 @@ Spree::OrderPopulator.class_eval do
|
||||
(variant_ids_in_cart - variant_ids_given).uniq
|
||||
end
|
||||
|
||||
def valid_variant?(variant)
|
||||
check_order_cycle_provided_for(variant) && check_variant_available_under_distribution(variant)
|
||||
end
|
||||
|
||||
def check_order_cycle_provided_for(variant)
|
||||
order_cycle_provided = (!order_cycle_required_for(variant) || @order_cycle.present?)
|
||||
errors.add(:base, "Please choose an order cycle for this order.") unless order_cycle_provided
|
||||
@@ -121,12 +139,10 @@ Spree::OrderPopulator.class_eval do
|
||||
end
|
||||
|
||||
def check_variant_available_under_distribution(variant)
|
||||
if DistributionChangeValidator.new(@order).variants_available_for_distribution(@distributor, @order_cycle).include? variant
|
||||
return true
|
||||
else
|
||||
errors.add(:base, I18n.t(:spree_order_populator_availability_error))
|
||||
return false
|
||||
end
|
||||
return true if DistributionChangeValidator.new(@order).variants_available_for_distribution(@distributor, @order_cycle).include? variant
|
||||
|
||||
errors.add(:base, I18n.t(:spree_order_populator_availability_error))
|
||||
false
|
||||
end
|
||||
|
||||
def order_cycle_required_for(variant)
|
||||
@@ -24,6 +24,14 @@ Openfoodnetwork::Application.routes.draw do
|
||||
get "/connect", to: redirect("https://openfoodnetwork.org/#{ENV['DEFAULT_COUNTRY_CODE'].andand.downcase}/connect/")
|
||||
get "/learn", to: redirect("https://openfoodnetwork.org/#{ENV['DEFAULT_COUNTRY_CODE'].andand.downcase}/learn/")
|
||||
|
||||
get "/cart", :to => "spree/orders#edit", :as => :cart
|
||||
put "/cart", :to => "spree/orders#update", :as => :update_cart
|
||||
put "/cart/empty", :to => 'spree/orders#empty', :as => :empty_cart
|
||||
|
||||
resource :cart, controller: "cart" do
|
||||
post :populate
|
||||
end
|
||||
|
||||
resource :shop, controller: "shop" do
|
||||
get :products
|
||||
post :order_cycle
|
||||
|
||||
108
spec/controllers/cart_controller_spec.rb
Normal file
108
spec/controllers/cart_controller_spec.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe CartController, type: :controller do
|
||||
let(:order) { create(:order) }
|
||||
|
||||
describe "returning stock levels in JSON on success" do
|
||||
let(:product) { create(:simple_product) }
|
||||
|
||||
it "returns stock levels as JSON" do
|
||||
allow(controller).to receive(:variant_ids_in) { [123] }
|
||||
allow(controller).to receive(:stock_levels) { 'my_stock_levels' }
|
||||
allow(CartService).to receive(:new).and_return(cart_service = double())
|
||||
allow(cart_service).to receive(:populate) { true }
|
||||
allow(cart_service).to receive(:variants_h) { {} }
|
||||
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
|
||||
data = JSON.parse(response.body)
|
||||
expect(data['stock_levels']).to eq('my_stock_levels')
|
||||
end
|
||||
|
||||
describe "generating stock levels" do
|
||||
let!(:order) { create(:order) }
|
||||
let!(:li) { create(:line_item, order: order, variant: v, quantity: 2, max_quantity: 3) }
|
||||
let!(:v) { create(:variant, count_on_hand: 4) }
|
||||
let!(:v2) { create(:variant, count_on_hand: 2) }
|
||||
|
||||
before do
|
||||
order.reload
|
||||
allow(controller).to receive(:current_order) { order }
|
||||
end
|
||||
|
||||
it "returns a hash with variant id, quantity, max_quantity and stock on hand" do
|
||||
expect(controller.stock_levels(order, [v.id])).to eq(
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4}}
|
||||
)
|
||||
end
|
||||
|
||||
it "includes all line items, even when the variant_id is not specified" do
|
||||
expect(controller.stock_levels(order, [])).to eq(
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4}}
|
||||
)
|
||||
end
|
||||
|
||||
it "includes an empty quantity entry for variants that aren't in the order" do
|
||||
expect(controller.stock_levels(order, [v.id, v2.id])).to eq(
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4},
|
||||
v2.id => {quantity: 0, max_quantity: 0, on_hand: 2}}
|
||||
)
|
||||
end
|
||||
|
||||
describe "encoding Infinity" do
|
||||
let!(:v) { create(:variant, on_demand: true, count_on_hand: 0) }
|
||||
|
||||
it "encodes Infinity as a large, finite integer" do
|
||||
expect(controller.stock_levels(order, [v.id])).to eq(
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "extracts variant ids from the cart service" do
|
||||
variants_h = [{:variant_id=>"900", :quantity=>2, :max_quantity=>nil},
|
||||
{:variant_id=>"940", :quantity=>3, :max_quantity=>3}]
|
||||
|
||||
expect(controller.variant_ids_in(variants_h)).to eq([900, 940])
|
||||
end
|
||||
end
|
||||
|
||||
context "adding a group buy product to the cart" do
|
||||
it "sets a variant attribute for the max quantity" do
|
||||
distributor_product = create(:distributor_enterprise)
|
||||
p = create(:product, :distributors => [distributor_product], :group_buy => true)
|
||||
|
||||
order = subject.current_order(true)
|
||||
allow(order).to receive(:distributor) { distributor_product }
|
||||
expect(order).to receive(:set_variant_attributes).with(p.master, {'max_quantity' => '3'})
|
||||
allow(controller).to receive(:current_order).and_return(order)
|
||||
|
||||
expect do
|
||||
spree_post :populate, variants: { p.master.id => 1 }, variant_attributes: { p.master.id => {max_quantity: 3 } }
|
||||
end.to change(Spree::LineItem, :count).by(1)
|
||||
end
|
||||
|
||||
it "returns HTTP success when successful" do
|
||||
allow(CartService).to receive(:new).and_return(cart_service = double())
|
||||
allow(cart_service).to receive(:populate) { true }
|
||||
allow(cart_service).to receive(:variants_h) { {} }
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "returns failure when unsuccessful" do
|
||||
allow(CartService).to receive(:new).and_return(cart_service = double())
|
||||
allow(cart_service).to receive(:populate).and_return false
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
expect(response.status).to eq(412)
|
||||
end
|
||||
|
||||
it "tells cart_service to overwrite" do
|
||||
allow(CartService).to receive(:new).and_return(cart_service = double())
|
||||
expect(cart_service).to receive(:populate).with({}, true)
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,40 +7,40 @@ describe Spree::OrdersController, type: :controller do
|
||||
|
||||
it "redirects home when no distributor is selected" do
|
||||
spree_get :edit
|
||||
response.should redirect_to root_path
|
||||
expect(response).to redirect_to root_path
|
||||
end
|
||||
|
||||
it "redirects to shop when order is empty" do
|
||||
controller.stub(:current_distributor).and_return(distributor)
|
||||
controller.stub(:current_order_cycle).and_return(order_cycle)
|
||||
controller.stub(:current_order).and_return order
|
||||
order.stub_chain(:line_items, :empty?).and_return true
|
||||
order.stub(:insufficient_stock_lines).and_return []
|
||||
allow(controller).to receive(:current_distributor).and_return(distributor)
|
||||
allow(controller).to receive(:current_order_cycle).and_return(order_cycle)
|
||||
allow(controller).to receive(:current_order).and_return order
|
||||
allow(order).to receive_message_chain(:line_items, :empty?).and_return true
|
||||
allow(order).to receive(:insufficient_stock_lines).and_return []
|
||||
session[:access_token] = order.token
|
||||
spree_get :edit
|
||||
response.should redirect_to shop_path
|
||||
expect(response).to redirect_to shop_path
|
||||
end
|
||||
|
||||
it "redirects to the shop when no order cycle is selected" do
|
||||
controller.stub(:current_distributor).and_return(distributor)
|
||||
allow(controller).to receive(:current_distributor).and_return(distributor)
|
||||
spree_get :edit
|
||||
response.should redirect_to shop_path
|
||||
expect(response).to redirect_to shop_path
|
||||
end
|
||||
|
||||
it "redirects home with message if hub is not ready for checkout" do
|
||||
VariantOverride.stub(:indexed).and_return({})
|
||||
allow(VariantOverride).to receive(:indexed).and_return({})
|
||||
|
||||
order = subject.current_order(true)
|
||||
distributor.stub(:ready_for_checkout?) { false }
|
||||
order.stub(distributor: distributor, order_cycle: order_cycle)
|
||||
allow(distributor).to receive(:ready_for_checkout?) { false }
|
||||
allow(order).to receive_messages(distributor: distributor, order_cycle: order_cycle)
|
||||
|
||||
order.should_receive(:empty!)
|
||||
order.should_receive(:set_distribution!).with(nil, nil)
|
||||
expect(order).to receive(:empty!)
|
||||
expect(order).to receive(:set_distribution!).with(nil, nil)
|
||||
|
||||
spree_get :edit
|
||||
|
||||
response.should redirect_to root_url
|
||||
flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later."
|
||||
expect(response).to redirect_to root_url
|
||||
expect(flash[:info]).to eq("The hub you have selected is temporarily closed for orders. Please try again later.")
|
||||
end
|
||||
|
||||
describe "when an item has insufficient stock" do
|
||||
@@ -59,105 +59,7 @@ describe Spree::OrdersController, type: :controller do
|
||||
it "displays a flash message when we view the cart" do
|
||||
spree_get :edit
|
||||
expect(response.status).to eq 200
|
||||
flash[:error].should == "An item in your cart has become unavailable."
|
||||
end
|
||||
end
|
||||
|
||||
describe "returning stock levels in JSON on success" do
|
||||
let(:product) { create(:simple_product) }
|
||||
|
||||
it "returns stock levels as JSON" do
|
||||
controller.stub(:variant_ids_in) { [123] }
|
||||
controller.stub(:stock_levels) { 'my_stock_levels' }
|
||||
Spree::OrderPopulator.stub(:new).and_return(populator = double())
|
||||
populator.stub(:populate) { true }
|
||||
populator.stub(:variants_h) { {} }
|
||||
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
|
||||
data = JSON.parse(response.body)
|
||||
data['stock_levels'].should == 'my_stock_levels'
|
||||
end
|
||||
|
||||
describe "generating stock levels" do
|
||||
let!(:order) { create(:order) }
|
||||
let!(:li) { create(:line_item, order: order, variant: v, quantity: 2, max_quantity: 3) }
|
||||
let!(:v) { create(:variant, count_on_hand: 4) }
|
||||
let!(:v2) { create(:variant, count_on_hand: 2) }
|
||||
|
||||
before do
|
||||
order.reload
|
||||
controller.stub(:current_order) { order }
|
||||
end
|
||||
|
||||
it "returns a hash with variant id, quantity, max_quantity and stock on hand" do
|
||||
controller.stock_levels(order, [v.id]).should ==
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4}}
|
||||
end
|
||||
|
||||
it "includes all line items, even when the variant_id is not specified" do
|
||||
controller.stock_levels(order, []).should ==
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4}}
|
||||
end
|
||||
|
||||
it "includes an empty quantity entry for variants that aren't in the order" do
|
||||
controller.stock_levels(order, [v.id, v2.id]).should ==
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4},
|
||||
v2.id => {quantity: 0, max_quantity: 0, on_hand: 2}}
|
||||
end
|
||||
|
||||
describe "encoding Infinity" do
|
||||
let!(:v) { create(:variant, on_demand: true, count_on_hand: 0) }
|
||||
|
||||
it "encodes Infinity as a large, finite integer" do
|
||||
controller.stock_levels(order, [v.id]).should ==
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "extracts variant ids from the populator" do
|
||||
variants_h = [{:variant_id=>"900", :quantity=>2, :max_quantity=>nil},
|
||||
{:variant_id=>"940", :quantity=>3, :max_quantity=>3}]
|
||||
|
||||
controller.variant_ids_in(variants_h).should == [900, 940]
|
||||
end
|
||||
end
|
||||
|
||||
context "adding a group buy product to the cart" do
|
||||
it "sets a variant attribute for the max quantity" do
|
||||
distributor_product = create(:distributor_enterprise)
|
||||
p = create(:product, :distributors => [distributor_product], :group_buy => true)
|
||||
|
||||
order = subject.current_order(true)
|
||||
order.stub(:distributor) { distributor_product }
|
||||
order.should_receive(:set_variant_attributes).with(p.master, {'max_quantity' => '3'})
|
||||
controller.stub(:current_order).and_return(order)
|
||||
|
||||
expect do
|
||||
spree_post :populate, :variants => {p.master.id => 1}, :variant_attributes => {p.master.id => {:max_quantity => 3}}
|
||||
end.to change(Spree::LineItem, :count).by(1)
|
||||
end
|
||||
|
||||
it "returns HTTP success when successful" do
|
||||
Spree::OrderPopulator.stub(:new).and_return(populator = double())
|
||||
populator.stub(:populate) { true }
|
||||
populator.stub(:variants_h) { {} }
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
response.status.should == 200
|
||||
end
|
||||
|
||||
it "returns failure when unsuccessful" do
|
||||
Spree::OrderPopulator.stub(:new).and_return(populator = double())
|
||||
populator.stub(:populate).and_return false
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
response.status.should == 412
|
||||
end
|
||||
|
||||
it "tells populator to overwrite" do
|
||||
Spree::OrderPopulator.stub(:new).and_return(populator = double())
|
||||
populator.should_receive(:populate).with({}, true)
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
expect(flash[:error]).to eq("An item in your cart has become unavailable.")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -170,8 +72,8 @@ describe Spree::OrdersController, type: :controller do
|
||||
"0" => {quantity: "0", id: "9999"},
|
||||
"1" => {quantity: "99", id: li.id}
|
||||
}}
|
||||
response.status.should == 302
|
||||
li.reload.quantity.should == 99
|
||||
expect(response.status).to eq(302)
|
||||
expect(li.reload.quantity).to eq(99)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -184,9 +86,9 @@ describe Spree::OrdersController, type: :controller do
|
||||
"1" => {quantity: "99", id: li.id}
|
||||
}
|
||||
|
||||
controller.remove_missing_line_items(attrs).should == {
|
||||
expect(controller.remove_missing_line_items(attrs)).to eq({
|
||||
"1" => {quantity: "99", id: li.id}
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -257,7 +159,7 @@ describe Spree::OrdersController, type: :controller do
|
||||
it "updates the fees" do
|
||||
expect(order.reload.adjustment_total).to eq enterprise_fee.calculator.preferred_amount
|
||||
|
||||
controller.stub spree_current_user: user
|
||||
allow(controller).to receive_messages spree_current_user: user
|
||||
spree_post :update, params
|
||||
|
||||
expect(order.reload.adjustment_total).to eq enterprise_fee.calculator.preferred_amount * 2
|
||||
|
||||
@@ -80,7 +80,7 @@ describe 'Cart service', ->
|
||||
data = {variants: {}}
|
||||
|
||||
it "sets update_running during the update, and clears it on success", ->
|
||||
$httpBackend.expectPOST("/orders/populate", data).respond 200, {}
|
||||
$httpBackend.expectPOST("/cart/populate", data).respond 200, {}
|
||||
expect(Cart.update_running).toBe(false)
|
||||
Cart.update()
|
||||
expect(Cart.update_running).toBe(true)
|
||||
@@ -88,7 +88,7 @@ describe 'Cart service', ->
|
||||
expect(Cart.update_running).toBe(false)
|
||||
|
||||
it "sets update_running during the update, and clears it on failure", ->
|
||||
$httpBackend.expectPOST("/orders/populate", data).respond 404, {}
|
||||
$httpBackend.expectPOST("/cart/populate", data).respond 404, {}
|
||||
expect(Cart.update_running).toBe(false)
|
||||
Cart.update()
|
||||
expect(Cart.update_running).toBe(true)
|
||||
@@ -97,7 +97,7 @@ describe 'Cart service', ->
|
||||
|
||||
it "marks the form as saved on success", ->
|
||||
spyOn(Cart, 'saved')
|
||||
$httpBackend.expectPOST("/orders/populate", data).respond 200, {}
|
||||
$httpBackend.expectPOST("/cart/populate", data).respond 200, {}
|
||||
Cart.update()
|
||||
$httpBackend.flush()
|
||||
expect(Cart.saved).toHaveBeenCalled()
|
||||
@@ -106,7 +106,7 @@ describe 'Cart service', ->
|
||||
Cart.update_enqueued = true
|
||||
spyOn(Cart, 'saved')
|
||||
spyOn(Cart, 'popQueue')
|
||||
$httpBackend.expectPOST("/orders/populate", data).respond 200, {}
|
||||
$httpBackend.expectPOST("/cart/populate", data).respond 200, {}
|
||||
Cart.update()
|
||||
$httpBackend.flush()
|
||||
expect(Cart.popQueue).toHaveBeenCalled()
|
||||
@@ -115,14 +115,14 @@ describe 'Cart service', ->
|
||||
Cart.update_enqueued = false
|
||||
spyOn(Cart, 'saved')
|
||||
spyOn(Cart, 'popQueue')
|
||||
$httpBackend.expectPOST("/orders/populate", data).respond 200, {}
|
||||
$httpBackend.expectPOST("/cart/populate", data).respond 200, {}
|
||||
Cart.update()
|
||||
$httpBackend.flush()
|
||||
expect(Cart.popQueue).not.toHaveBeenCalled()
|
||||
|
||||
it "retries the update on failure", ->
|
||||
spyOn(Cart, 'scheduleRetry')
|
||||
$httpBackend.expectPOST("/orders/populate", data).respond 404, {}
|
||||
$httpBackend.expectPOST("/cart/populate", data).respond 404, {}
|
||||
Cart.update()
|
||||
$httpBackend.flush()
|
||||
expect(Cart.scheduleRetry).toHaveBeenCalled()
|
||||
|
||||
@@ -36,8 +36,8 @@ describe ProductDistribution do
|
||||
|
||||
# When I add the product to the order, an adjustment should be made
|
||||
expect do
|
||||
op = Spree::OrderPopulator.new order, 'AU'
|
||||
op.populate products: {product.id => product.master.id}, quantity: 1, distributor_id: distributor.id
|
||||
cart_service = CartService.new order
|
||||
cart_service.populate products: {product.id => product.master.id}, quantity: 1, distributor_id: distributor.id
|
||||
|
||||
# Normally the controller would fire this event when the order's contents are changed
|
||||
fire_order_contents_changed_event(order.user, order)
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe OrderPopulator do
|
||||
let(:order) { double(:order, id: 123) }
|
||||
let(:currency) { double(:currency) }
|
||||
let(:params) { {} }
|
||||
let(:distributor) { double(:distributor) }
|
||||
let(:order_cycle) { double(:order_cycle) }
|
||||
let(:op) { OrderPopulator.new(order, currency) }
|
||||
|
||||
context "end-to-end" do
|
||||
let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) }
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], variants: [v]) }
|
||||
let(:op) { OrderPopulator.new(order, nil) }
|
||||
let(:v) { create(:variant) }
|
||||
|
||||
describe "populate" do
|
||||
it "adds a variant" do
|
||||
op.populate({variants: {v.id.to_s => {quantity: '1', max_quantity: '2'}}}, true)
|
||||
li = order.find_line_item_by_variant(v)
|
||||
li.should be
|
||||
li.quantity.should == 1
|
||||
li.max_quantity.should == 2
|
||||
li.final_weight_volume.should == 1.0
|
||||
end
|
||||
|
||||
it "updates a variant's quantity, max quantity and final_weight_volume" do
|
||||
order.add_variant v, 1, 2
|
||||
|
||||
op.populate({variants: {v.id.to_s => {quantity: '2', max_quantity: '3'}}}, true)
|
||||
li = order.find_line_item_by_variant(v)
|
||||
li.should be
|
||||
li.quantity.should == 2
|
||||
li.max_quantity.should == 3
|
||||
li.final_weight_volume.should == 2.0
|
||||
end
|
||||
|
||||
it "removes a variant" do
|
||||
order.add_variant v, 1, 2
|
||||
|
||||
op.populate({variants: {}}, true)
|
||||
order.line_items(:reload)
|
||||
li = order.find_line_item_by_variant(v)
|
||||
li.should_not be
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "populate" do
|
||||
before do
|
||||
op.should_receive(:distributor_and_order_cycle).
|
||||
and_return([distributor, order_cycle])
|
||||
end
|
||||
|
||||
it "checks that distribution can supply all products in the cart" do
|
||||
op.should_receive(:distribution_can_supply_products_in_cart).
|
||||
with(distributor, order_cycle).and_return(false)
|
||||
|
||||
op.populate(params).should be false
|
||||
op.errors.to_a.should == ["That distributor or order cycle can't supply all the products in your cart. Please choose another."]
|
||||
end
|
||||
|
||||
it "locks the order" do
|
||||
op.stub(:distribution_can_supply_products_in_cart).and_return(true)
|
||||
order.should_receive(:with_lock)
|
||||
op.populate(params, true)
|
||||
end
|
||||
|
||||
it "attempts cart add with max_quantity" do
|
||||
op.stub(:distribution_can_supply_products_in_cart).and_return true
|
||||
params = {variants: {"1" => {quantity: 1, max_quantity: 2}}}
|
||||
order.stub(:with_lock).and_yield
|
||||
op.stub(:varies_from_cart) { true }
|
||||
op.stub(:variants_removed) { [] }
|
||||
op.should_receive(:attempt_cart_add).with("1", 1, 2).and_return true
|
||||
op.populate(params, true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "varies_from_cart" do
|
||||
let(:variant) { double(:variant, id: 123) }
|
||||
|
||||
it "returns true when item is not in cart and a quantity is specified" do
|
||||
op.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil)
|
||||
op.send(:varies_from_cart, {variant_id: variant.id, quantity: '2'}).should be true
|
||||
end
|
||||
|
||||
it "returns true when item is not in cart and a max_quantity is specified" do
|
||||
op.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil)
|
||||
op.send(:varies_from_cart, {variant_id: variant.id, quantity: '0', max_quantity: '2'}).should be true
|
||||
end
|
||||
|
||||
it "returns false when item is not in cart and no quantity or max_quantity are specified" do
|
||||
op.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil)
|
||||
op.send(:varies_from_cart, {variant_id: variant.id, quantity: '0'}).should be false
|
||||
end
|
||||
|
||||
it "returns true when quantity varies" do
|
||||
li = double(:line_item, quantity: 1, max_quantity: nil)
|
||||
op.stub(:line_item_for_variant_id) { li }
|
||||
|
||||
op.send(:varies_from_cart, {variant_id: variant.id, quantity: '2'}).should be true
|
||||
end
|
||||
|
||||
it "returns true when max_quantity varies" do
|
||||
li = double(:line_item, quantity: 1, max_quantity: nil)
|
||||
op.stub(:line_item_for_variant_id) { li }
|
||||
|
||||
op.send(:varies_from_cart, {variant_id: variant.id, quantity: '1', max_quantity: '3'}).should be true
|
||||
end
|
||||
|
||||
it "returns false when max_quantity varies only in nil vs 0" do
|
||||
li = double(:line_item, quantity: 1, max_quantity: nil)
|
||||
op.stub(:line_item_for_variant_id) { li }
|
||||
|
||||
op.send(:varies_from_cart, {variant_id: variant.id, quantity: '1'}).should be false
|
||||
end
|
||||
|
||||
it "returns false when both are specified and neither varies" do
|
||||
li = double(:line_item, quantity: 1, max_quantity: 2)
|
||||
op.stub(:line_item_for_variant_id) { li }
|
||||
|
||||
op.send(:varies_from_cart, {variant_id: variant.id, quantity: '1', max_quantity: '2'}).should be false
|
||||
end
|
||||
end
|
||||
|
||||
describe "variants_removed" do
|
||||
it "returns the variant ids when one is in the cart but not in those given" do
|
||||
op.stub(:variant_ids_in_cart) { [123] }
|
||||
op.send(:variants_removed, []).should == [123]
|
||||
end
|
||||
|
||||
it "returns nothing when all items in the cart are provided" do
|
||||
op.stub(:variant_ids_in_cart) { [123] }
|
||||
op.send(:variants_removed, [{variant_id: '123'}]).should == []
|
||||
end
|
||||
|
||||
it "returns nothing when items are added to cart" do
|
||||
op.stub(:variant_ids_in_cart) { [123] }
|
||||
op.send(:variants_removed, [{variant_id: '123'}, {variant_id: '456'}]).should == []
|
||||
end
|
||||
|
||||
it "does not return duplicates" do
|
||||
op.stub(:variant_ids_in_cart) { [123, 123] }
|
||||
op.send(:variants_removed, []).should == [123]
|
||||
end
|
||||
end
|
||||
|
||||
describe "attempt_cart_add" do
|
||||
let(:variant) { double(:variant, on_hand: 250) }
|
||||
let(:quantity) { 123 }
|
||||
|
||||
before do
|
||||
Spree::Variant.stub(:find).and_return(variant)
|
||||
VariantOverride.stub(:for).and_return(nil)
|
||||
end
|
||||
|
||||
it "performs additional validations" do
|
||||
op.should_receive(:check_order_cycle_provided_for).with(variant).and_return(true)
|
||||
op.should_receive(:check_variant_available_under_distribution).with(variant).
|
||||
and_return(true)
|
||||
order.should_receive(:add_variant).with(variant, quantity, nil, currency)
|
||||
|
||||
op.attempt_cart_add(333, quantity.to_s)
|
||||
end
|
||||
|
||||
it "filters quantities through #quantities_to_add" do
|
||||
op.should_receive(:quantities_to_add).with(variant, 123, 123).
|
||||
and_return([5, 5])
|
||||
|
||||
op.stub(:check_order_cycle_provided_for) { true }
|
||||
op.stub(:check_variant_available_under_distribution) { true }
|
||||
|
||||
order.should_receive(:add_variant).with(variant, 5, 5, currency)
|
||||
|
||||
op.attempt_cart_add(333, quantity.to_s, quantity.to_s)
|
||||
end
|
||||
|
||||
it "removes variants which have become out of stock" do
|
||||
op.should_receive(:quantities_to_add).with(variant, 123, 123).
|
||||
and_return([0, 0])
|
||||
|
||||
op.stub(:check_order_cycle_provided_for) { true }
|
||||
op.stub(:check_variant_available_under_distribution) { true }
|
||||
|
||||
order.should_receive(:remove_variant).with(variant)
|
||||
order.should_receive(:add_variant).never
|
||||
|
||||
op.attempt_cart_add(333, quantity.to_s, quantity.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
describe "quantities_to_add" do
|
||||
let(:v) { double(:variant, on_hand: 10) }
|
||||
|
||||
context "when backorders are not allowed" do
|
||||
context "when max_quantity is not provided" do
|
||||
it "returns full amount when available" do
|
||||
op.quantities_to_add(v, 5, nil).should == [5, nil]
|
||||
end
|
||||
|
||||
it "returns a limited amount when not entirely available" do
|
||||
op.quantities_to_add(v, 15, nil).should == [10, nil]
|
||||
end
|
||||
end
|
||||
|
||||
context "when max_quantity is provided" do
|
||||
it "returns full amount when available" do
|
||||
op.quantities_to_add(v, 5, 6).should == [5, 6]
|
||||
end
|
||||
|
||||
it "also returns the full amount when not entirely available" do
|
||||
op.quantities_to_add(v, 15, 16).should == [10, 16]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when backorders are allowed" do
|
||||
before do
|
||||
Spree::Config.allow_backorders = true
|
||||
end
|
||||
|
||||
it "does not limit quantity" do
|
||||
op.quantities_to_add(v, 15, nil).should == [15, nil]
|
||||
end
|
||||
|
||||
it "does not limit max_quantity" do
|
||||
op.quantities_to_add(v, 15, 16).should == [15, 16]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
describe "determining if distributor can supply products in cart" do
|
||||
it "delegates to DistributionChangeValidator" do
|
||||
dcv = double(:dcv)
|
||||
dcv.should_receive(:can_change_to_distribution?).with(distributor, order_cycle).and_return(true)
|
||||
DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv)
|
||||
op.send(:distribution_can_supply_products_in_cart, distributor, order_cycle).should be true
|
||||
end
|
||||
end
|
||||
|
||||
describe "checking order cycle is provided for a variant, OR is not needed" do
|
||||
let(:variant) { double(:variant) }
|
||||
|
||||
it "returns false and errors when order cycle is not provided and is required" do
|
||||
op.stub(:order_cycle_required_for).and_return true
|
||||
op.send(:check_order_cycle_provided_for, variant).should be false
|
||||
op.errors.to_a.should == ["Please choose an order cycle for this order."]
|
||||
end
|
||||
it "returns true when order cycle is provided" do
|
||||
op.stub(:order_cycle_required_for).and_return true
|
||||
op.instance_variable_set :@order_cycle, double(:order_cycle)
|
||||
op.send(:check_order_cycle_provided_for, variant).should be true
|
||||
end
|
||||
it "returns true when order cycle is not required" do
|
||||
op.stub(:order_cycle_required_for).and_return false
|
||||
op.send(:check_order_cycle_provided_for, variant).should be true
|
||||
end
|
||||
end
|
||||
|
||||
describe "checking variant is available under the distributor" do
|
||||
let(:product) { double(:product) }
|
||||
let(:variant) { double(:variant, product: product) }
|
||||
|
||||
it "delegates to DistributionChangeValidator, returning true when available" do
|
||||
dcv = double(:dcv)
|
||||
dcv.should_receive(:variants_available_for_distribution).with(123, 234).and_return([variant])
|
||||
DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv)
|
||||
op.instance_eval { @distributor = 123; @order_cycle = 234 }
|
||||
op.send(:check_variant_available_under_distribution, variant).should be true
|
||||
op.errors.should be_empty
|
||||
end
|
||||
|
||||
it "delegates to DistributionChangeValidator, returning false and erroring otherwise" do
|
||||
dcv = double(:dcv)
|
||||
dcv.should_receive(:variants_available_for_distribution).with(123, 234).and_return([])
|
||||
DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv)
|
||||
op.instance_eval { @distributor = 123; @order_cycle = 234 }
|
||||
op.send(:check_variant_available_under_distribution, variant).should be false
|
||||
op.errors.to_a.should == ["That product is not available from the chosen distributor or order cycle."]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "support" do
|
||||
describe "checking if order cycle is required for a variant" do
|
||||
it "requires an order cycle when the product has no product distributions" do
|
||||
product = double(:product, product_distributions: [])
|
||||
variant = double(:variant, product: product)
|
||||
op.send(:order_cycle_required_for, variant).should be true
|
||||
end
|
||||
|
||||
it "does not require an order cycle when the product has product distributions" do
|
||||
product = double(:product, product_distributions: [1])
|
||||
variant = double(:variant, product: product)
|
||||
op.send(:order_cycle_required_for, variant).should be false
|
||||
end
|
||||
end
|
||||
|
||||
it "provides the distributor and order cycle for the order" do
|
||||
order.should_receive(:distributor).and_return(distributor)
|
||||
order.should_receive(:order_cycle).and_return(order_cycle)
|
||||
op.send(:distributor_and_order_cycle).should == [distributor,
|
||||
order_cycle]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
314
spec/services/cart_service_spec.rb
Normal file
314
spec/services/cart_service_spec.rb
Normal file
@@ -0,0 +1,314 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe CartService do
|
||||
let(:order) { double(:order, id: 123) }
|
||||
let(:currency) { double(:currency) }
|
||||
let(:params) { {} }
|
||||
let(:distributor) { double(:distributor) }
|
||||
let(:order_cycle) { double(:order_cycle) }
|
||||
let(:cart_service) { CartService.new(order) }
|
||||
|
||||
before do
|
||||
allow(order).to receive(:currency).and_return( currency )
|
||||
end
|
||||
|
||||
context "end-to-end" do
|
||||
let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) }
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], variants: [v]) }
|
||||
let(:cart_service) { CartService.new(order) }
|
||||
let(:v) { create(:variant) }
|
||||
|
||||
describe "populate" do
|
||||
it "adds a variant" do
|
||||
cart_service.populate({variants: {v.id.to_s => {quantity: '1', max_quantity: '2'}}}, true)
|
||||
li = order.find_line_item_by_variant(v)
|
||||
expect(li).to be
|
||||
expect(li.quantity).to eq(1)
|
||||
expect(li.max_quantity).to eq(2)
|
||||
expect(li.final_weight_volume).to eq(1.0)
|
||||
end
|
||||
|
||||
it "updates a variant's quantity, max quantity and final_weight_volume" do
|
||||
order.add_variant v, 1, 2
|
||||
|
||||
cart_service.populate({variants: {v.id.to_s => {quantity: '2', max_quantity: '3'}}}, true)
|
||||
li = order.find_line_item_by_variant(v)
|
||||
expect(li).to be
|
||||
expect(li.quantity).to eq(2)
|
||||
expect(li.max_quantity).to eq(3)
|
||||
expect(li.final_weight_volume).to eq(2.0)
|
||||
end
|
||||
|
||||
it "removes a variant" do
|
||||
order.add_variant v, 1, 2
|
||||
|
||||
cart_service.populate({variants: {}}, true)
|
||||
order.line_items(:reload)
|
||||
li = order.find_line_item_by_variant(v)
|
||||
expect(li).not_to be
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "populate" do
|
||||
before do
|
||||
expect(cart_service).to receive(:distributor_and_order_cycle).
|
||||
and_return([distributor, order_cycle])
|
||||
end
|
||||
|
||||
it "checks that distribution can supply all products in the cart" do
|
||||
expect(cart_service).to receive(:distribution_can_supply_products_in_cart).
|
||||
with(distributor, order_cycle).and_return(false)
|
||||
|
||||
expect(cart_service.populate(params)).to be false
|
||||
expect(cart_service.errors.to_a).to eq(["That distributor or order cycle can't supply all the products in your cart. Please choose another."])
|
||||
end
|
||||
|
||||
it "locks the order" do
|
||||
allow(cart_service).to receive(:distribution_can_supply_products_in_cart).and_return(true)
|
||||
expect(order).to receive(:with_lock)
|
||||
cart_service.populate(params, true)
|
||||
end
|
||||
|
||||
it "attempts cart add with max_quantity" do
|
||||
allow(cart_service).to receive(:distribution_can_supply_products_in_cart).and_return true
|
||||
params = {variants: {"1" => {quantity: 1, max_quantity: 2}}}
|
||||
allow(order).to receive(:with_lock).and_yield
|
||||
allow(cart_service).to receive(:varies_from_cart) { true }
|
||||
allow(cart_service).to receive(:variants_removed) { [] }
|
||||
expect(cart_service).to receive(:attempt_cart_add).with("1", 1, 2).and_return true
|
||||
cart_service.populate(params, true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "varies_from_cart" do
|
||||
let(:variant) { double(:variant, id: 123) }
|
||||
|
||||
it "returns true when item is not in cart and a quantity is specified" do
|
||||
expect(cart_service).to receive(:line_item_for_variant_id).with(variant.id).and_return(nil)
|
||||
expect(cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '2'})).to be true
|
||||
end
|
||||
|
||||
it "returns true when item is not in cart and a max_quantity is specified" do
|
||||
expect(cart_service).to receive(:line_item_for_variant_id).with(variant.id).and_return(nil)
|
||||
expect(cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '0', max_quantity: '2'})).to be true
|
||||
end
|
||||
|
||||
it "returns false when item is not in cart and no quantity or max_quantity are specified" do
|
||||
expect(cart_service).to receive(:line_item_for_variant_id).with(variant.id).and_return(nil)
|
||||
expect(cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '0'})).to be false
|
||||
end
|
||||
|
||||
it "returns true when quantity varies" do
|
||||
li = double(:line_item, quantity: 1, max_quantity: nil)
|
||||
allow(cart_service).to receive(:line_item_for_variant_id) { li }
|
||||
|
||||
expect(cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '2'})).to be true
|
||||
end
|
||||
|
||||
it "returns true when max_quantity varies" do
|
||||
li = double(:line_item, quantity: 1, max_quantity: nil)
|
||||
allow(cart_service).to receive(:line_item_for_variant_id) { li }
|
||||
|
||||
expect(cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '1', max_quantity: '3'})).to be true
|
||||
end
|
||||
|
||||
it "returns false when max_quantity varies only in nil vs 0" do
|
||||
li = double(:line_item, quantity: 1, max_quantity: nil)
|
||||
allow(cart_service).to receive(:line_item_for_variant_id) { li }
|
||||
|
||||
expect(cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '1'})).to be false
|
||||
end
|
||||
|
||||
it "returns false when both are specified and neither varies" do
|
||||
li = double(:line_item, quantity: 1, max_quantity: 2)
|
||||
allow(cart_service).to receive(:line_item_for_variant_id) { li }
|
||||
|
||||
expect(cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '1', max_quantity: '2'})).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe "variants_removed" do
|
||||
it "returns the variant ids when one is in the cart but not in those given" do
|
||||
allow(cart_service).to receive(:variant_ids_in_cart) { [123] }
|
||||
expect(cart_service.send(:variants_removed, [])).to eq([123])
|
||||
end
|
||||
|
||||
it "returns nothing when all items in the cart are provided" do
|
||||
allow(cart_service).to receive(:variant_ids_in_cart) { [123] }
|
||||
expect(cart_service.send(:variants_removed, [{variant_id: '123'}])).to eq([])
|
||||
end
|
||||
|
||||
it "returns nothing when items are added to cart" do
|
||||
allow(cart_service).to receive(:variant_ids_in_cart) { [123] }
|
||||
expect(cart_service.send(:variants_removed, [{variant_id: '123'}, {variant_id: '456'}])).to eq([])
|
||||
end
|
||||
|
||||
it "does not return duplicates" do
|
||||
allow(cart_service).to receive(:variant_ids_in_cart) { [123, 123] }
|
||||
expect(cart_service.send(:variants_removed, [])).to eq([123])
|
||||
end
|
||||
end
|
||||
|
||||
describe "attempt_cart_add" do
|
||||
let(:variant) { double(:variant, on_hand: 250) }
|
||||
let(:quantity) { 123 }
|
||||
|
||||
before do
|
||||
allow(Spree::Variant).to receive(:find).and_return(variant)
|
||||
allow(VariantOverride).to receive(:for).and_return(nil)
|
||||
end
|
||||
|
||||
it "performs additional validations" do
|
||||
expect(cart_service).to receive(:check_order_cycle_provided_for).with(variant).and_return(true)
|
||||
expect(cart_service).to receive(:check_variant_available_under_distribution).with(variant).
|
||||
and_return(true)
|
||||
expect(order).to receive(:add_variant).with(variant, quantity, nil, currency)
|
||||
|
||||
cart_service.attempt_cart_add(333, quantity.to_s)
|
||||
end
|
||||
|
||||
it "filters quantities through #quantities_to_add" do
|
||||
expect(cart_service).to receive(:quantities_to_add).with(variant, 123, 123).
|
||||
and_return([5, 5])
|
||||
|
||||
allow(cart_service).to receive(:check_order_cycle_provided_for) { true }
|
||||
allow(cart_service).to receive(:check_variant_available_under_distribution) { true }
|
||||
|
||||
expect(order).to receive(:add_variant).with(variant, 5, 5, currency)
|
||||
|
||||
cart_service.attempt_cart_add(333, quantity.to_s, quantity.to_s)
|
||||
end
|
||||
|
||||
it "removes variants which have become out of stock" do
|
||||
expect(cart_service).to receive(:quantities_to_add).with(variant, 123, 123).
|
||||
and_return([0, 0])
|
||||
|
||||
allow(cart_service).to receive(:check_order_cycle_provided_for) { true }
|
||||
allow(cart_service).to receive(:check_variant_available_under_distribution) { true }
|
||||
|
||||
expect(order).to receive(:remove_variant).with(variant)
|
||||
expect(order).to receive(:add_variant).never
|
||||
|
||||
cart_service.attempt_cart_add(333, quantity.to_s, quantity.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
describe "quantities_to_add" do
|
||||
let(:v) { double(:variant, on_hand: 10) }
|
||||
|
||||
context "when backorders are not allowed" do
|
||||
context "when max_quantity is not provided" do
|
||||
it "returns full amount when available" do
|
||||
expect(cart_service.quantities_to_add(v, 5, nil)).to eq([5, nil])
|
||||
end
|
||||
|
||||
it "returns a limited amount when not entirely available" do
|
||||
expect(cart_service.quantities_to_add(v, 15, nil)).to eq([10, nil])
|
||||
end
|
||||
end
|
||||
|
||||
context "when max_quantity is provided" do
|
||||
it "returns full amount when available" do
|
||||
expect(cart_service.quantities_to_add(v, 5, 6)).to eq([5, 6])
|
||||
end
|
||||
|
||||
it "also returns the full amount when not entirely available" do
|
||||
expect(cart_service.quantities_to_add(v, 15, 16)).to eq([10, 16])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when backorders are allowed" do
|
||||
before do
|
||||
Spree::Config.allow_backorders = true
|
||||
end
|
||||
|
||||
it "does not limit quantity" do
|
||||
expect(cart_service.quantities_to_add(v, 15, nil)).to eq([15, nil])
|
||||
end
|
||||
|
||||
it "does not limit max_quantity" do
|
||||
expect(cart_service.quantities_to_add(v, 15, 16)).to eq([15, 16])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
describe "determining if distributor can supply products in cart" do
|
||||
it "delegates to DistributionChangeValidator" do
|
||||
dcv = double(:dcv)
|
||||
expect(dcv).to receive(:can_change_to_distribution?).with(distributor, order_cycle).and_return(true)
|
||||
expect(DistributionChangeValidator).to receive(:new).with(order).and_return(dcv)
|
||||
expect(cart_service.send(:distribution_can_supply_products_in_cart, distributor, order_cycle)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe "checking order cycle is provided for a variant, OR is not needed" do
|
||||
let(:variant) { double(:variant) }
|
||||
|
||||
it "returns false and errors when order cycle is not provided and is required" do
|
||||
allow(cart_service).to receive(:order_cycle_required_for).and_return true
|
||||
expect(cart_service.send(:check_order_cycle_provided_for, variant)).to be false
|
||||
expect(cart_service.errors.to_a).to eq(["Please choose an order cycle for this order."])
|
||||
end
|
||||
it "returns true when order cycle is provided" do
|
||||
allow(cart_service).to receive(:order_cycle_required_for).and_return true
|
||||
cart_service.instance_variable_set :@order_cycle, double(:order_cycle)
|
||||
expect(cart_service.send(:check_order_cycle_provided_for, variant)).to be true
|
||||
end
|
||||
it "returns true when order cycle is not required" do
|
||||
allow(cart_service).to receive(:order_cycle_required_for).and_return false
|
||||
expect(cart_service.send(:check_order_cycle_provided_for, variant)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe "checking variant is available under the distributor" do
|
||||
let(:product) { double(:product) }
|
||||
let(:variant) { double(:variant, product: product) }
|
||||
|
||||
it "delegates to DistributionChangeValidator, returning true when available" do
|
||||
dcv = double(:dcv)
|
||||
expect(dcv).to receive(:variants_available_for_distribution).with(123, 234).and_return([variant])
|
||||
expect(DistributionChangeValidator).to receive(:new).with(order).and_return(dcv)
|
||||
cart_service.instance_eval { @distributor = 123; @order_cycle = 234 }
|
||||
expect(cart_service.send(:check_variant_available_under_distribution, variant)).to be true
|
||||
expect(cart_service.errors).to be_empty
|
||||
end
|
||||
|
||||
it "delegates to DistributionChangeValidator, returning false and erroring otherwise" do
|
||||
dcv = double(:dcv)
|
||||
expect(dcv).to receive(:variants_available_for_distribution).with(123, 234).and_return([])
|
||||
expect(DistributionChangeValidator).to receive(:new).with(order).and_return(dcv)
|
||||
cart_service.instance_eval { @distributor = 123; @order_cycle = 234 }
|
||||
expect(cart_service.send(:check_variant_available_under_distribution, variant)).to be false
|
||||
expect(cart_service.errors.to_a).to eq(["That product is not available from the chosen distributor or order cycle."])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "support" do
|
||||
describe "checking if order cycle is required for a variant" do
|
||||
it "requires an order cycle when the product has no product distributions" do
|
||||
product = double(:product, product_distributions: [])
|
||||
variant = double(:variant, product: product)
|
||||
expect(cart_service.send(:order_cycle_required_for, variant)).to be true
|
||||
end
|
||||
|
||||
it "does not require an order cycle when the product has product distributions" do
|
||||
product = double(:product, product_distributions: [1])
|
||||
variant = double(:variant, product: product)
|
||||
expect(cart_service.send(:order_cycle_required_for, variant)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
it "provides the distributor and order cycle for the order" do
|
||||
expect(order).to receive(:distributor).and_return(distributor)
|
||||
expect(order).to receive(:order_cycle).and_return(order_cycle)
|
||||
expect(cart_service.send(:distributor_and_order_cycle)).to eq([distributor,
|
||||
order_cycle])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -17,8 +17,8 @@ module ShopWorkflow
|
||||
end
|
||||
|
||||
def add_product_to_cart(order, product, quantity: 1)
|
||||
populator = Spree::OrderPopulator.new(order, order.currency)
|
||||
populator.populate(variants: {product.variants.first.id => quantity})
|
||||
cart_service = CartService.new(order)
|
||||
cart_service.populate(variants: {product.variants.first.id => quantity})
|
||||
|
||||
# Recalculate fee totals
|
||||
order.update_distribution_charge!
|
||||
|
||||
Reference in New Issue
Block a user