mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-05 22:26:07 +00:00
Spree made that scope default so that we don't need to define or call
it. There might be cases in which we were showing deleted variants and
now we are not, but I have not idea how to find them.
Related Spree commit:
- cd3add960e
530 lines
20 KiB
Ruby
530 lines
20 KiB
Ruby
require 'spec_helper'
|
|
require 'open_food_network/option_value_namer'
|
|
require 'open_food_network/products_cache'
|
|
|
|
module Spree
|
|
describe Variant do
|
|
describe "double loading" do
|
|
# app/models/spree/variant_decorator.rb may be double-loaded in delayed job environment,
|
|
# so we need to be able to do so without error.
|
|
it "succeeds without error" do
|
|
load "#{Rails.root}/app/models/spree/variant_decorator.rb"
|
|
end
|
|
end
|
|
|
|
describe "scopes" do
|
|
describe "finding variants in a distributor" do
|
|
let!(:d1) { create(:distributor_enterprise) }
|
|
let!(:d2) { create(:distributor_enterprise) }
|
|
let!(:p1) { create(:simple_product) }
|
|
let!(:p2) { create(:simple_product) }
|
|
let!(:oc1) { create(:simple_order_cycle, distributors: [d1], variants: [p1.master]) }
|
|
let!(:oc2) { create(:simple_order_cycle, distributors: [d2], variants: [p2.master]) }
|
|
|
|
it "shows variants in an order cycle distribution" do
|
|
Variant.in_distributor(d1).should == [p1.master]
|
|
end
|
|
|
|
it "doesn't show duplicates" do
|
|
oc_dup = create(:simple_order_cycle, distributors: [d1], variants: [p1.master])
|
|
Variant.in_distributor(d1).should == [p1.master]
|
|
end
|
|
end
|
|
|
|
describe "finding variants in an order cycle" do
|
|
let!(:d1) { create(:distributor_enterprise) }
|
|
let!(:d2) { create(:distributor_enterprise) }
|
|
let!(:p1) { create(:product) }
|
|
let!(:p2) { create(:product) }
|
|
let!(:oc1) { create(:simple_order_cycle, distributors: [d1], variants: [p1.master]) }
|
|
let!(:oc2) { create(:simple_order_cycle, distributors: [d2], variants: [p2.master]) }
|
|
|
|
it "shows variants in an order cycle" do
|
|
Variant.in_order_cycle(oc1).should == [p1.master]
|
|
end
|
|
|
|
it "doesn't show duplicates" do
|
|
ex = create(:exchange, order_cycle: oc1, sender: oc1.coordinator, receiver: d2)
|
|
ex.variants << p1.master
|
|
|
|
Variant.in_order_cycle(oc1).should == [p1.master]
|
|
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_distribution(oc, d1).should == [v1]
|
|
p2.variants.for_distribution(oc, d2).should == [v2]
|
|
end
|
|
|
|
it "does not return variants in the order cycle but not the distributor" do
|
|
p1.variants.for_distribution(oc, d2).should be_empty
|
|
p2.variants.for_distribution(oc, d1).should be_empty
|
|
end
|
|
|
|
it "does not return variants not in the order cycle" do
|
|
p_external.variants.for_distribution(oc, d1).should be_empty
|
|
end
|
|
end
|
|
|
|
describe "finding variants based on visiblity in inventory" do
|
|
let(:enterprise) { create(:distributor_enterprise) }
|
|
let!(:new_variant) { create(:variant) }
|
|
let!(:hidden_variant) { create(:variant) }
|
|
let!(:visible_variant) { create(:variant) }
|
|
|
|
let!(:hidden_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: hidden_variant, visible: false ) }
|
|
let!(:visible_inventory_item) { create(:inventory_item, enterprise: enterprise, variant: visible_variant, visible: true ) }
|
|
|
|
context "finding variants that are not hidden from an enterprise's inventory" do
|
|
context "when the enterprise given is nil" do
|
|
let!(:variants) { Spree::Variant.not_hidden_for(nil) }
|
|
|
|
it "returns an empty list" do
|
|
expect(variants).to eq []
|
|
end
|
|
end
|
|
|
|
context "when an enterprise is given" do
|
|
let!(:variants) { Spree::Variant.not_hidden_for(enterprise) }
|
|
|
|
it "lists any variants that are not listed as visible=false" do
|
|
expect(variants).to include new_variant, visible_variant
|
|
expect(variants).to_not include hidden_variant
|
|
end
|
|
|
|
context "when inventory items exist for other enterprises" do
|
|
let(:other_enterprise) { create(:distributor_enterprise) }
|
|
|
|
let!(:new_inventory_item) { create(:inventory_item, enterprise: other_enterprise, variant: new_variant, visible: true ) }
|
|
let!(:hidden_inventory_item2) { create(:inventory_item, enterprise: other_enterprise, variant: visible_variant, visible: false ) }
|
|
let!(:visible_inventory_item2) { create(:inventory_item, enterprise: other_enterprise, variant: hidden_variant, visible: true ) }
|
|
|
|
it "lists any variants that are not listed as visible=false only for the relevant enterprise" do
|
|
expect(variants).to include new_variant, visible_variant
|
|
expect(variants).to_not include hidden_variant
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "finding variants that are visible in an enterprise's inventory" do
|
|
let!(:variants) { Spree::Variant.visible_for(enterprise) }
|
|
|
|
it "lists any variants that are listed as visible=true" do
|
|
expect(variants).to include visible_variant
|
|
expect(variants).to_not include new_variant, hidden_variant
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'stockable_by' do
|
|
let(:shop) { create(:distributor_enterprise) }
|
|
let(:add_to_oc_producer) { create(:supplier_enterprise) }
|
|
let(:other_producer) { create(:supplier_enterprise) }
|
|
let!(:v1) { create(:variant, product: create(:simple_product, supplier: shop ) ) }
|
|
let!(:v2) { create(:variant, product: create(:simple_product, supplier: add_to_oc_producer ) ) }
|
|
let!(:v3) { create(:variant, product: create(:simple_product, supplier: other_producer ) ) }
|
|
|
|
before do
|
|
create(:enterprise_relationship, parent: add_to_oc_producer, child: shop, permissions_list: [:add_to_order_cycle])
|
|
create(:enterprise_relationship, parent: other_producer, child: shop, permissions_list: [:manage_products])
|
|
end
|
|
|
|
it 'shows variants produced by the enterprise and any producers granting P-OC' do
|
|
stockable_variants = Spree::Variant.stockable_by(shop)
|
|
expect(stockable_variants).to include v1, v2
|
|
expect(stockable_variants).to_not include v3
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "callbacks" do
|
|
let(:variant) { create(:variant) }
|
|
|
|
it "refreshes the products cache on save" do
|
|
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).with(variant)
|
|
variant.sku = 'abc123'
|
|
variant.save
|
|
end
|
|
|
|
it "refreshes the products cache on destroy" do
|
|
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
|
|
variant.destroy
|
|
end
|
|
|
|
context "when it is the master variant" do
|
|
let(:product) { create(:simple_product) }
|
|
let(:master) { product.master }
|
|
|
|
it "refreshes the products cache for the entire product on save" do
|
|
expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product)
|
|
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_changed).never
|
|
master.sku = 'abc123'
|
|
master.save
|
|
end
|
|
|
|
it "refreshes the products cache for the entire product on destroy" do
|
|
# Does this ever happen?
|
|
expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product)
|
|
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).never
|
|
master.destroy
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "indexing variants by id" do
|
|
let!(:v1) { create(:variant) }
|
|
let!(:v2) { create(:variant) }
|
|
let!(:v3) { create(:variant) }
|
|
|
|
it "indexes variants by id" do
|
|
Variant.where(id: [v1, v2, v3]).indexed.should ==
|
|
{v1.id => v1, v2.id => v2, v3.id => v3}
|
|
end
|
|
end
|
|
|
|
describe "generating the product and variant name" do
|
|
let(:v) { Variant.new }
|
|
let(:p) { double(:product, name: 'product') }
|
|
before { allow(v).to receive(:product) { p } }
|
|
|
|
context "when full_name starts with the product name" do
|
|
before { allow(v).to receive(:full_name) { p.name + " - something" } }
|
|
|
|
it "does not show the product name twice" do
|
|
v.product_and_full_name.should == 'product - something'
|
|
end
|
|
end
|
|
|
|
context "when full_name does not start with the product name" do
|
|
before { allow(v).to receive(:full_name) { "display_name (unit)" } }
|
|
|
|
it "prepends the product name to the full name" do
|
|
v.product_and_full_name.should == 'product - display_name (unit)'
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "calculating the price with enterprise fees" do
|
|
it "returns the price plus the fees" do
|
|
distributor = double(:distributor)
|
|
order_cycle = double(:order_cycle)
|
|
|
|
variant = Variant.new price: 100
|
|
variant.should_receive(:fees_for).with(distributor, order_cycle) { 23 }
|
|
variant.price_with_fees(distributor, order_cycle).should == 123
|
|
end
|
|
end
|
|
|
|
|
|
describe "calculating the fees" do
|
|
it "delegates to EnterpriseFeeCalculator" do
|
|
distributor = double(:distributor)
|
|
order_cycle = double(:order_cycle)
|
|
variant = Variant.new
|
|
|
|
OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.should_receive(:fees_for).with(variant) { 23 }
|
|
|
|
variant.fees_for(distributor, order_cycle).should == 23
|
|
end
|
|
end
|
|
|
|
|
|
describe "calculating fees broken down by fee type" do
|
|
it "delegates to EnterpriseFeeCalculator" do
|
|
distributor = double(:distributor)
|
|
order_cycle = double(:order_cycle)
|
|
variant = Variant.new
|
|
fees = double(:fees)
|
|
|
|
OpenFoodNetwork::EnterpriseFeeCalculator.any_instance.should_receive(:fees_by_type_for).with(variant) { fees }
|
|
|
|
variant.fees_by_type_for(distributor, order_cycle).should == fees
|
|
end
|
|
end
|
|
|
|
|
|
context "when the product has variants" do
|
|
let!(:product) { create(:simple_product) }
|
|
let!(:variant) { create(:variant, product: product) }
|
|
|
|
%w(weight volume).each do |unit|
|
|
context "when the product's unit is #{unit}" do
|
|
before do
|
|
product.update_attribute :variant_unit, unit
|
|
product.reload
|
|
end
|
|
|
|
it "is valid when unit value is set and unit description is not" do
|
|
variant.unit_value = 1
|
|
variant.unit_description = nil
|
|
variant.should be_valid
|
|
end
|
|
|
|
it "is invalid when unit value is not set" do
|
|
variant.unit_value = nil
|
|
variant.should_not be_valid
|
|
end
|
|
|
|
it "has a valid master variant" do
|
|
product.master.should be_valid
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the product's unit is items" do
|
|
before do
|
|
product.update_attribute :variant_unit, 'items'
|
|
product.reload
|
|
end
|
|
|
|
it "is valid with only unit value set" do
|
|
variant.unit_value = 1
|
|
variant.unit_description = nil
|
|
variant.should be_valid
|
|
end
|
|
|
|
it "is valid with only unit description set" do
|
|
variant.unit_value = nil
|
|
variant.unit_description = 'Medium'
|
|
variant.should be_valid
|
|
end
|
|
|
|
it "is invalid when neither unit value nor unit description are set" do
|
|
variant.unit_value = nil
|
|
variant.unit_description = nil
|
|
variant.should_not be_valid
|
|
end
|
|
|
|
it "has a valid master variant" do
|
|
product.master.should be_valid
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "unit value/description" do
|
|
describe "generating the full name" do
|
|
let(:v) { Variant.new }
|
|
|
|
before do
|
|
v.stub(:display_name) { 'display_name' }
|
|
v.stub(:unit_to_display) { 'unit_to_display' }
|
|
end
|
|
|
|
it "returns unit_to_display when display_name is blank" do
|
|
v.stub(:display_name) { '' }
|
|
v.full_name.should == 'unit_to_display'
|
|
end
|
|
|
|
it "returns display_name when it contains unit_to_display" do
|
|
v.stub(:display_name) { 'DiSpLaY_name' }
|
|
v.stub(:unit_to_display) { 'name' }
|
|
v.full_name.should == 'DiSpLaY_name'
|
|
end
|
|
|
|
it "returns unit_to_display when it contains display_name" do
|
|
v.stub(:display_name) { '_to_' }
|
|
v.stub(:unit_to_display) { 'unit_TO_display' }
|
|
v.full_name.should == 'unit_TO_display'
|
|
end
|
|
|
|
it "returns a combination otherwise" do
|
|
v.stub(:display_name) { 'display_name' }
|
|
v.stub(:unit_to_display) { 'unit_to_display' }
|
|
v.full_name.should == 'display_name (unit_to_display)'
|
|
end
|
|
|
|
it "is resilient to regex chars" do
|
|
v = Variant.new display_name: ")))"
|
|
v.stub(:unit_to_display) { ")))" }
|
|
v.full_name.should == ")))"
|
|
end
|
|
end
|
|
|
|
describe "getting name for display" do
|
|
it "returns display_name if present" do
|
|
v = create(:variant, display_name: "foo")
|
|
v.name_to_display.should == "foo"
|
|
end
|
|
|
|
it "returns product name if display_name is empty" do
|
|
v = create(:variant, product: create(:product))
|
|
v.name_to_display.should == v.product.name
|
|
v1 = create(:variant, display_name: "", product: create(:product))
|
|
v1.name_to_display.should == v1.product.name
|
|
end
|
|
end
|
|
|
|
describe "getting unit for display" do
|
|
it "returns display_as if present" do
|
|
v = create(:variant, display_as: "foo")
|
|
v.unit_to_display.should == "foo"
|
|
end
|
|
|
|
it "returns options_text if display_as is blank" do
|
|
v = create(:variant)
|
|
v1 = create(:variant, display_as: "")
|
|
v.stub(:options_text).and_return "ponies"
|
|
v1.stub(:options_text).and_return "ponies"
|
|
v.unit_to_display.should == "ponies"
|
|
v1.unit_to_display.should == "ponies"
|
|
end
|
|
end
|
|
|
|
describe "setting the variant's weight from the unit value" do
|
|
it "sets the variant's weight when unit is weight" do
|
|
p = create(:simple_product, variant_unit: 'volume')
|
|
v = create(:variant, product: p, weight: nil)
|
|
|
|
p.update_attributes! variant_unit: 'weight', variant_unit_scale: 1
|
|
v.update_attributes! unit_value: 10, unit_description: 'foo'
|
|
|
|
v.reload.weight.should == 0.01
|
|
end
|
|
|
|
it "does nothing when unit is not weight" do
|
|
p = create(:simple_product, variant_unit: 'volume')
|
|
v = create(:variant, product: p, weight: 123)
|
|
|
|
p.update_attributes! variant_unit: 'volume', variant_unit_scale: 1
|
|
v.update_attributes! unit_value: 10, unit_description: 'foo'
|
|
|
|
v.reload.weight.should == 123
|
|
end
|
|
|
|
it "does nothing when unit_value is not set" do
|
|
p = create(:simple_product, variant_unit: 'volume')
|
|
v = create(:variant, product: p, weight: 123)
|
|
|
|
p.update_attributes! variant_unit: 'weight', variant_unit_scale: 1
|
|
|
|
# Although invalid, this calls the before_validation callback, which would
|
|
# error if not handling unit_value == nil case
|
|
v.update_attributes(unit_value: nil, unit_description: 'foo').should be false
|
|
|
|
v.reload.weight.should == 123
|
|
end
|
|
end
|
|
|
|
context "when the variant already has a value set (and all required option values do not exist)" do
|
|
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
|
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar') }
|
|
|
|
it "removes the old option value and assigns the new one" do
|
|
ov_orig = v.option_values.last
|
|
|
|
expect {
|
|
v.update_attributes!(unit_value: 10, unit_description: 'foo')
|
|
}.to change(Spree::OptionValue, :count).by(1)
|
|
|
|
v.option_values.should_not include ov_orig
|
|
end
|
|
end
|
|
|
|
context "when the variant already has a value set (and all required option values exist)" do
|
|
let!(:p0) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
|
let!(:v0) { create(:variant, product: p0, unit_value: 10, unit_description: 'foo') }
|
|
|
|
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
|
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar') }
|
|
|
|
it "removes the old option value and assigns the new one" do
|
|
ov_orig = v.option_values.last
|
|
ov_new = v0.option_values.last
|
|
|
|
expect {
|
|
v.update_attributes!(unit_value: 10, unit_description: 'foo')
|
|
}.to change(Spree::OptionValue, :count).by(0)
|
|
|
|
v.option_values.should_not include ov_orig
|
|
v.option_values.should include ov_new
|
|
end
|
|
end
|
|
|
|
context "when the variant does not have a display_as value set" do
|
|
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
|
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: '') }
|
|
|
|
it "requests the name of the new option_value from OptionValueName" do
|
|
OpenFoodNetwork::OptionValueNamer.any_instance.should_receive(:name).exactly(1).times.and_call_original
|
|
v.update_attributes(unit_value: 10, unit_description: 'foo')
|
|
ov = v.option_values.last
|
|
ov.name.should == "10g foo"
|
|
end
|
|
end
|
|
|
|
context "when the variant has a display_as value set" do
|
|
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
|
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: 'FOOS!') }
|
|
|
|
it "does not request the name of the new option_value from OptionValueName" do
|
|
OpenFoodNetwork::OptionValueNamer.any_instance.should_not_receive(:name)
|
|
v.update_attributes!(unit_value: 10, unit_description: 'foo')
|
|
ov = v.option_values.last
|
|
ov.name.should == "FOOS!"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "deleting unit option values" do
|
|
before do
|
|
p = create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1)
|
|
ot = Spree::OptionType.find_by_name 'unit_weight'
|
|
@v = create(:variant, product: p)
|
|
end
|
|
|
|
it "removes option value associations for unit option types" do
|
|
expect {
|
|
@v.delete_unit_option_values
|
|
}.to change(@v.option_values, :count).by(-1)
|
|
end
|
|
|
|
it "does not delete option values" do
|
|
expect {
|
|
@v.delete_unit_option_values
|
|
}.to change(Spree::OptionValue, :count).by(0)
|
|
end
|
|
end
|
|
|
|
context "extends LocalizedNumber" do
|
|
subject! { build(:variant) }
|
|
|
|
it_behaves_like "a model using the LocalizedNumber module", [:price, :cost_price, :weight]
|
|
end
|
|
end
|
|
|
|
describe "destruction" do
|
|
it "destroys exchange variants" do
|
|
v = create(:variant)
|
|
e = create(:exchange, variants: [v])
|
|
|
|
v.destroy
|
|
e.reload.variant_ids.should be_empty
|
|
end
|
|
end
|
|
end
|