mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
552 lines
20 KiB
Ruby
552 lines
20 KiB
Ruby
require 'spec_helper'
|
|
|
|
module Spree
|
|
describe Product do
|
|
|
|
describe "associations" do
|
|
it { should belong_to(:supplier) }
|
|
it { should have_many(:product_distributions) }
|
|
end
|
|
|
|
describe "validations and defaults" do
|
|
it "is valid when created from factory" do
|
|
create(:product).should be_valid
|
|
end
|
|
|
|
it "requires a supplier" do
|
|
product = create(:simple_product)
|
|
product.supplier = nil
|
|
product.should_not be_valid
|
|
end
|
|
|
|
it "should default available_on to now" do
|
|
Timecop.freeze
|
|
product = Product.new
|
|
product.available_on.should == Time.now
|
|
end
|
|
|
|
context "when the product has variants" do
|
|
let(:product) do
|
|
product = create(:simple_product)
|
|
create(:variant, product: product)
|
|
product.reload
|
|
end
|
|
|
|
it "requires a unit" do
|
|
product.variant_unit = nil
|
|
product.should_not be_valid
|
|
end
|
|
|
|
%w(weight volume).each do |unit|
|
|
context "when unit is #{unit}" do
|
|
it "is valid when unit scale is set and unit name is not" do
|
|
product.variant_unit = unit
|
|
product.variant_unit_scale = 1
|
|
product.variant_unit_name = nil
|
|
product.should be_valid
|
|
end
|
|
|
|
it "is invalid when unit scale is not set" do
|
|
product.variant_unit = unit
|
|
product.variant_unit_scale = nil
|
|
product.variant_unit_name = nil
|
|
product.should_not be_valid
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the unit is items" do
|
|
it "is valid when unit name is set and unit scale is not" do
|
|
product.variant_unit = 'items'
|
|
product.variant_unit_name = 'loaf'
|
|
product.variant_unit_scale = nil
|
|
product.should be_valid
|
|
end
|
|
|
|
it "is invalid when unit name is not set" do
|
|
product.variant_unit = 'items'
|
|
product.variant_unit_name = nil
|
|
product.variant_unit_scale = nil
|
|
product.should_not be_valid
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when product does not have variants" do
|
|
let(:product) { create(:simple_product) }
|
|
|
|
it "does not require any variant unit fields" do
|
|
product.variant_unit = nil
|
|
product.variant_unit_name = nil
|
|
product.variant_unit_scale = nil
|
|
|
|
product.should be_valid
|
|
end
|
|
|
|
it "requires a unit scale when variant unit is weight" do
|
|
product.variant_unit = 'weight'
|
|
product.variant_unit_scale = nil
|
|
product.variant_unit_name = nil
|
|
|
|
product.should_not be_valid
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
describe "scopes" do
|
|
describe "in_supplier" do
|
|
it "shows products in supplier" do
|
|
s1 = create(:supplier_enterprise)
|
|
s2 = create(:supplier_enterprise)
|
|
p1 = create(:product, supplier: s1)
|
|
p2 = create(:product, supplier: s2)
|
|
Product.in_supplier(s1).should == [p1]
|
|
end
|
|
end
|
|
|
|
describe "in_distributor" do
|
|
it "shows products in product distribution" do
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product, distributors: [d1])
|
|
p2 = create(:product, distributors: [d2])
|
|
Product.in_distributor(d1).should == [p1]
|
|
end
|
|
|
|
it "shows products in order cycle distribution" do
|
|
s = create(:supplier_enterprise)
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product)
|
|
p2 = create(:product)
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [p1.master])
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master])
|
|
Product.in_distributor(d1).should == [p1]
|
|
end
|
|
|
|
it "shows products in order cycle distribution by variant" do
|
|
s = create(:supplier_enterprise)
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product)
|
|
v1 = create(:variant, product: p1)
|
|
p2 = create(:product)
|
|
v2 = create(:variant, product: p2)
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [v1])
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [v2])
|
|
Product.in_distributor(d1).should == [p1]
|
|
end
|
|
|
|
it "doesn't show products listed in the incoming exchange only", :future => true do
|
|
s = create(:supplier_enterprise)
|
|
c = create(:distributor_enterprise)
|
|
d = create(:distributor_enterprise)
|
|
p = create(:product)
|
|
oc = create(:simple_order_cycle, coordinator: c, suppliers: [s], distributors: [d])
|
|
ex = oc.exchanges.incoming.first
|
|
ex.variants << p.master
|
|
|
|
Product.in_distributor(d).should be_empty
|
|
end
|
|
|
|
it "shows products in both without duplicates" do
|
|
s = create(:supplier_enterprise)
|
|
d = create(:distributor_enterprise)
|
|
p = create(:product, distributors: [d])
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d], variants: [p.master])
|
|
Product.in_distributor(d).should == [p]
|
|
end
|
|
end
|
|
|
|
describe "in_product_distribution_by" do
|
|
it "shows products in product distribution" do
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product, distributors: [d1])
|
|
p2 = create(:product, distributors: [d2])
|
|
Product.in_product_distribution_by(d1).should == [p1]
|
|
end
|
|
|
|
it "does not show products in order cycle distribution" do
|
|
s = create(:supplier_enterprise)
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product)
|
|
p2 = create(:product)
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [p1.master])
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master])
|
|
Product.in_product_distribution_by(d1).should == []
|
|
end
|
|
end
|
|
|
|
describe "in_supplier_or_distributor" do
|
|
it "shows products in supplier" do
|
|
s1 = create(:supplier_enterprise)
|
|
s2 = create(:supplier_enterprise)
|
|
p1 = create(:product, supplier: s1)
|
|
p2 = create(:product, supplier: s2)
|
|
Product.in_supplier_or_distributor(s1).should == [p1]
|
|
end
|
|
|
|
it "shows products in product distribution" do
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product, distributors: [d1])
|
|
p2 = create(:product, distributors: [d2])
|
|
Product.in_supplier_or_distributor(d1).should == [p1]
|
|
end
|
|
|
|
it "shows products in order cycle distribution" do
|
|
s = create(:supplier_enterprise)
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product)
|
|
p2 = create(:product)
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [p1.master])
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master])
|
|
Product.in_supplier_or_distributor(d1).should == [p1]
|
|
end
|
|
|
|
it "shows products in all three without duplicates" do
|
|
s = create(:supplier_enterprise)
|
|
d = create(:distributor_enterprise)
|
|
p = create(:product, supplier: s, distributors: [d])
|
|
create(:simple_order_cycle, suppliers: [s], distributors: [d], variants: [p.master])
|
|
[s, d].each { |e| Product.in_supplier_or_distributor(e).should == [p] }
|
|
end
|
|
end
|
|
|
|
describe "in_order_cycle" do
|
|
it "shows products in order cycle distribution" do
|
|
s = create(:supplier_enterprise)
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product)
|
|
p2 = create(:product)
|
|
oc1 = create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [p1.master])
|
|
oc2 = create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master])
|
|
Product.in_order_cycle(oc1).should == [p1]
|
|
end
|
|
end
|
|
|
|
describe "access roles" do
|
|
before(:each) do
|
|
@e1 = create(:enterprise)
|
|
@e2 = create(:enterprise)
|
|
@p1 = create(:product, supplier: @e1)
|
|
@p2 = create(:product, supplier: @e2)
|
|
end
|
|
|
|
it "shows only products for given user" do
|
|
user = create(:user)
|
|
user.spree_roles = []
|
|
@e1.enterprise_roles.build(user: user).save
|
|
|
|
product = Product.managed_by user
|
|
product.count.should == 1
|
|
product.should include @p1
|
|
end
|
|
|
|
it "shows all products for admin user" do
|
|
user = create(:admin_user)
|
|
|
|
product = Product.managed_by user
|
|
product.count.should == 2
|
|
product.should include @p1
|
|
product.should include @p2
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "finders" do
|
|
it "finds the product distribution for a particular distributor" do
|
|
distributor = create(:distributor_enterprise)
|
|
product = create(:product)
|
|
product_distribution = create(:product_distribution, product: product, distributor: distributor)
|
|
product.product_distribution_for(distributor).should == product_distribution
|
|
end
|
|
end
|
|
|
|
describe "membership" do
|
|
it "queries its membership of a particular product distribution" do
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p = create(:product, distributors: [d1])
|
|
|
|
p.should be_in_distributor d1
|
|
p.should_not be_in_distributor d2
|
|
end
|
|
|
|
it "queries its membership of a particular order cycle distribution" do
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product)
|
|
p2 = create(:product)
|
|
oc1 = create(:order_cycle, :distributors => [d1], :variants => [p1.master])
|
|
oc2 = create(:order_cycle, :distributors => [d2], :variants => [p2.master])
|
|
|
|
p1.should be_in_distributor d1
|
|
p1.should_not be_in_distributor d2
|
|
end
|
|
|
|
it "queries its membership of a particular order cycle" do
|
|
d1 = create(:distributor_enterprise)
|
|
d2 = create(:distributor_enterprise)
|
|
p1 = create(:product)
|
|
p2 = create(:product)
|
|
oc1 = create(:order_cycle, :distributors => [d1], :variants => [p1.master])
|
|
oc2 = create(:order_cycle, :distributors => [d2], :variants => [p2.master])
|
|
|
|
p1.should be_in_order_cycle oc1
|
|
p1.should_not be_in_order_cycle oc2
|
|
end
|
|
end
|
|
|
|
describe "finding variants for an order cycle and hub" do
|
|
let(:oc) { create(:simple_order_cycle) }
|
|
let(:s) { create(:supplier_enterprise) }
|
|
let(:d1) { create(:distributor_enterprise) }
|
|
let(:d2) { create(:distributor_enterprise) }
|
|
|
|
let(:p1) { create(:simple_product) }
|
|
let(:p2) { create(:simple_product) }
|
|
let(:v1) { create(:variant, product: p1) }
|
|
let(:v2) { create(:variant, product: p2) }
|
|
|
|
let(:p_external) { create(:simple_product) }
|
|
let(:v_external) { create(:variant, product: p_external) }
|
|
|
|
let!(:ex_in) { create(:exchange, order_cycle: oc, sender: s, receiver: oc.coordinator,
|
|
incoming: true, variants: [v1, v2]) }
|
|
let!(:ex_out1) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d1,
|
|
incoming: false, variants: [v1]) }
|
|
let!(:ex_out2) { create(:exchange, order_cycle: oc, sender: oc.coordinator, receiver: d2,
|
|
incoming: false, variants: [v2]) }
|
|
|
|
it "returns variants in the order cycle and distributor" do
|
|
p1.variants_for(oc, d1).should == [v1]
|
|
p2.variants_for(oc, d2).should == [v2]
|
|
end
|
|
|
|
it "does not return variants in the order cycle but not the distributor" do
|
|
p1.variants_for(oc, d2).should be_empty
|
|
p2.variants_for(oc, d1).should be_empty
|
|
end
|
|
|
|
it "does not return variants not in the order cycle" do
|
|
p_external.variants_for(oc, d1).should be_empty
|
|
end
|
|
end
|
|
|
|
describe "variant units" do
|
|
context "when the product initially has no variant unit" do
|
|
let!(:p) { create(:simple_product,
|
|
variant_unit: nil,
|
|
variant_unit_scale: nil,
|
|
variant_unit_name: nil) }
|
|
|
|
context "when the required option type does not exist" do
|
|
it "creates the option type and assigns it to the product" do
|
|
expect {
|
|
p.update_attributes!(variant_unit: 'weight', variant_unit_scale: 1000)
|
|
}.to change(Spree::OptionType, :count).by(1)
|
|
|
|
ot = Spree::OptionType.last
|
|
ot.name.should == 'unit_weight'
|
|
ot.presentation.should == 'Weight'
|
|
|
|
p.option_types.should == [ot]
|
|
end
|
|
|
|
it "does the same with volume" do
|
|
expect {
|
|
p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 1000)
|
|
}.to change(Spree::OptionType, :count).by(1)
|
|
|
|
ot = Spree::OptionType.last
|
|
ot.name.should == 'unit_volume'
|
|
ot.presentation.should == 'Volume'
|
|
|
|
p.option_types.should == [ot]
|
|
end
|
|
|
|
it "does the same with items" do
|
|
expect {
|
|
p.update_attributes!(variant_unit: 'items', variant_unit_name: 'packet')
|
|
}.to change(Spree::OptionType, :count).by(1)
|
|
|
|
ot = Spree::OptionType.last
|
|
ot.name.should == 'unit_items'
|
|
ot.presentation.should == 'Items'
|
|
|
|
p.option_types.should == [ot]
|
|
end
|
|
end
|
|
|
|
context "when the required option type already exists" do
|
|
let!(:ot) { create(:option_type, name: 'unit_weight', presentation: 'Weight') }
|
|
|
|
it "looks up the option type and assigns it to the product" do
|
|
expect {
|
|
p.update_attributes!(variant_unit: 'weight', variant_unit_scale: 1000)
|
|
}.to change(Spree::OptionType, :count).by(0)
|
|
|
|
p.option_types.should == [ot]
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the product already has a variant unit set (and all required option types exist)" do
|
|
let!(:p) { create(:simple_product,
|
|
variant_unit: 'weight',
|
|
variant_unit_scale: 1,
|
|
variant_unit_name: nil) }
|
|
|
|
let!(:ot_volume) { create(:option_type, name: 'unit_volume', presentation: 'Volume') }
|
|
|
|
it "removes the old option type and assigns the new one" do
|
|
p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 0.001)
|
|
p.option_types.should == [ot_volume]
|
|
end
|
|
|
|
it "leaves option type unassigned if none is provided" do
|
|
p.update_attributes!(variant_unit: nil, variant_unit_scale: nil)
|
|
p.option_types.should == []
|
|
end
|
|
|
|
it "does not remove and re-add the option type if it is not changed" do
|
|
p.option_types.should_receive(:delete).never
|
|
p.update_attributes!(name: 'foo')
|
|
end
|
|
|
|
it "removes the related option values from all its variants" do
|
|
ot = Spree::OptionType.find_by_name 'unit_weight'
|
|
v = create(:variant, product: p)
|
|
p.reload
|
|
|
|
expect {
|
|
p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 0.001)
|
|
}.to change(v.option_values(true), :count).by(-1)
|
|
end
|
|
|
|
it "removes the related option values from its master variant" do
|
|
ot = Spree::OptionType.find_by_name 'unit_weight'
|
|
p.master.update_attributes!(unit_value: 1)
|
|
p.reload
|
|
|
|
expect {
|
|
p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 0.001)
|
|
}.to change(p.master.option_values(true), :count).by(-1)
|
|
end
|
|
end
|
|
|
|
describe "returning the variant unit option type" do
|
|
it "returns nil when variant_unit is not set" do
|
|
p = create(:simple_product, variant_unit: nil)
|
|
p.variant_unit_option_type.should be_nil
|
|
end
|
|
end
|
|
|
|
it "finds all variant unit option types" do
|
|
ot1 = create(:option_type, name: 'unit_weight', presentation: 'Weight')
|
|
ot2 = create(:option_type, name: 'unit_volume', presentation: 'Volume')
|
|
ot3 = create(:option_type, name: 'unit_items', presentation: 'Items')
|
|
ot4 = create(:option_type, name: 'foo_unit_bar', presentation: 'Foo')
|
|
|
|
Spree::Product.all_variant_unit_option_types.sort.should == [ot1, ot2, ot3].sort
|
|
end
|
|
end
|
|
|
|
describe "option types" do
|
|
describe "removing an option type" do
|
|
it "removes the associated option values from all variants" do
|
|
# Given a product with a variant unit option type and values
|
|
p = create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1)
|
|
v1 = create(:variant, product: p, unit_value: 100, option_values: [])
|
|
v2 = create(:variant, product: p, unit_value: 200, option_values: [])
|
|
|
|
# And a custom option type and values
|
|
ot = create(:option_type, name: 'foo', presentation: 'foo')
|
|
p.option_types << ot
|
|
ov1 = create(:option_value, option_type: ot, name: 'One', presentation: 'One')
|
|
ov2 = create(:option_value, option_type: ot, name: 'Two', presentation: 'Two')
|
|
v1.option_values << ov1
|
|
v2.option_values << ov2
|
|
|
|
# When we remove the custom option type
|
|
p.option_type_ids = p.option_type_ids.reject { |id| id == ot.id }
|
|
|
|
# Then the associated option values should have been removed from the variants
|
|
v1.option_values(true).should_not include ov1
|
|
v2.option_values(true).should_not include ov2
|
|
|
|
# And the option values themselves should still exist
|
|
Spree::OptionValue.where(id: [ov1.id, ov2.id]).count.should == 2
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "stock filtering" do
|
|
it "considers products that are on_demand as being in stock" do
|
|
product = create(:simple_product, on_demand: true)
|
|
product.master.update_attribute(:count_on_hand, 0)
|
|
product.has_stock?.should == true
|
|
end
|
|
|
|
describe "finding products in stock for a particular distribution" do
|
|
it "returns in-stock products without variants" do
|
|
p = create(:simple_product)
|
|
p.master.update_attribute(:count_on_hand, 1)
|
|
d = create(:distributor_enterprise)
|
|
oc = create(:simple_order_cycle, distributors: [d])
|
|
oc.exchanges.outgoing.first.variants << p.master
|
|
|
|
p.should have_stock_for_distribution(oc, d)
|
|
end
|
|
|
|
it "returns on-demand products" do
|
|
p = create(:simple_product, on_demand: true)
|
|
p.master.update_attribute(:count_on_hand, 0)
|
|
d = create(:distributor_enterprise)
|
|
oc = create(:simple_order_cycle, distributors: [d])
|
|
oc.exchanges.outgoing.first.variants << p.master
|
|
|
|
p.should have_stock_for_distribution(oc, d)
|
|
end
|
|
|
|
it "returns products with in-stock variants" do
|
|
p = create(:simple_product)
|
|
v = create(:variant, product: p)
|
|
v.update_attribute(:count_on_hand, 1)
|
|
d = create(:distributor_enterprise)
|
|
oc = create(:simple_order_cycle, distributors: [d])
|
|
oc.exchanges.outgoing.first.variants << v
|
|
|
|
p.should have_stock_for_distribution(oc, d)
|
|
end
|
|
|
|
it "returns products with on-demand variants" do
|
|
p = create(:simple_product)
|
|
v = create(:variant, product: p, on_demand: true)
|
|
v.update_attribute(:count_on_hand, 0)
|
|
d = create(:distributor_enterprise)
|
|
oc = create(:simple_order_cycle, distributors: [d])
|
|
oc.exchanges.outgoing.first.variants << v
|
|
|
|
p.should have_stock_for_distribution(oc, d)
|
|
end
|
|
|
|
it "does not return products that have stock not in the distribution" do
|
|
p = create(:simple_product)
|
|
p.master.update_attribute(:count_on_hand, 1)
|
|
d = create(:distributor_enterprise)
|
|
oc = create(:simple_order_cycle, distributors: [d])
|
|
|
|
p.should_not have_stock_for_distribution(oc, d)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|