require 'spec_helper' module Spree describe Product do describe "associations" do it { is_expected.to belong_to(:supplier) } it { is_expected.to belong_to(:primary_taxon) } end describe "validations and defaults" do it "is valid when built from factory" do expect(build(:product)).to be_valid end it "requires a primary taxon" do expect(build(:simple_product, taxons: [], primary_taxon: nil)).not_to be_valid end it "requires a supplier" do expect(build(:simple_product, supplier: nil)).not_to be_valid end it "does not save when master is invalid" do product = build(:product) product.variant_unit = 'weight' product.master.unit_value = nil expect(product.save).to eq(false) end it "defaults available_on to now" do Timecop.freeze do product = Product.new expect(product.available_on).to eq(Time.zone.now) end end describe "permalink" do let(:name) { "Banana Permanenta" } it "generates a unique permalink" do product1 = create(:product, name: "Banana Permanenta", permalink: nil) product2 = create(:product, name: "Banana Permanenta", permalink: nil) expect(product2.permalink).to_not eq product1.permalink # "banana-permanenta" != "banana-permanenta-1" # generated by Spree end it "generates a unique permalink considering deleted products" do product1 = create(:product, name: "Banana Permanenta", permalink: nil) product1.destroy product2 = create(:product, name: "Banana Permanenta", permalink: nil) expect(product2.permalink).to_not eq product1.permalink # "banana-permanenta" != "banana-permanenta1" # generated by OFN end end describe "tax category" do context "when a tax category is required" do it "is invalid when a tax category is not provided" do with_products_require_tax_category(true) do expect(build(:product, tax_category_id: nil)).not_to be_valid end end end context "when a tax category is not required" do it "is valid when a tax category is not provided" do with_products_require_tax_category(false) do expect(build(:product, tax_category_id: nil)).to be_valid end end end 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 expect(product).not_to 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 expect(product).to 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 expect(product).not_to be_valid end end end context "saving a new product" do let!(:product){ Spree::Product.new } before do create(:stock_location) product.primary_taxon = create(:taxon) product.supplier = create(:supplier_enterprise) product.name = "Product1" product.variant_unit = "weight" product.variant_unit_scale = 1000 product.unit_value = 1 product.price = 4.27 product.shipping_category = create(:shipping_category) product.save! end it "copies the properties on master variant to the first standard variant" do expect(product.variants(:reload).length).to eq 1 standard_variant = product.variants(:reload).first expect(standard_variant.price).to eq product.master.price end it "only duplicates master with after_save when no standard variants exist" do expect(product).to receive :ensure_standard_variant product.name = "Something else" expect{ product.save! }.to_not change{ product.variants.count } 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 expect(product).to 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 expect(product).not_to be_valid end end end context "a basic product" do let(:product) { create(:simple_product) } it "requires variant unit fields" do product.variant_unit = nil product.variant_unit_name = nil product.variant_unit_scale = nil expect(product).to be_invalid 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 expect(product).not_to be_valid end end end describe "callbacks" do let(:product) { create(:simple_product) } it "refreshes the products cache on save" do expect(OpenFoodNetwork::ProductsCache).to receive(:product_changed).with(product) product.name = 'asdf' product.save end it "refreshes the products cache on delete" do expect(OpenFoodNetwork::ProductsCache).to receive(:product_deleted).with(product) product.destroy end # On destroy, all distributed variants are refreshed by a Variant around_destroy # callback, so we don't need to do anything on the product model. describe "touching affected enterprises when the product is deleted" do let(:product) { create(:simple_product) } let(:supplier) { product.supplier } let(:distributor) { create(:distributor_enterprise) } let!(:oc) { create(:simple_order_cycle, distributors: [distributor], variants: [product.variants.first]) } it "touches the supplier" do expect { product.destroy }.to change { supplier.reload.updated_at } end it "touches all distributors" do expect { product.destroy }.to change { distributor.reload.updated_at } end it "removes variants from order cycles" do expect { product.destroy }.to change { ExchangeVariant.count } end end it "adds the primary taxon to the product's taxon list" do taxon = create(:taxon) product = create(:product, primary_taxon: taxon) expect(product.taxons).to include(taxon) end it "removes the previous primary taxon from the taxon list" do original_taxon = create(:taxon) product = create(:product, primary_taxon: original_taxon) product.primary_taxon = create(:taxon) product.save! expect(product.taxons).not_to include(original_taxon) 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) expect(Product.in_supplier(s1)).to eq([p1]) end end describe "in_distributor" 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) create(:simple_order_cycle, suppliers: [s], distributors: [d1], variants: [p1.master]) create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master]) expect(Product.in_distributor(d1)).to eq([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]) expect(Product.in_distributor(d1)).to eq([p1]) end it "doesn't show products listed in the incoming exchange only" 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 expect(Product.in_distributor(d)).to be_empty 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) expect(Product.in_supplier_or_distributor(s1)).to eq([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]) expect(Product.in_supplier_or_distributor(d1)).to eq([p1]) end it "shows products in all three without duplicates" do s = create(:supplier_enterprise) d = create(:distributor_enterprise) p = create(:product, supplier: s) create(:simple_order_cycle, suppliers: [s], distributors: [d], variants: [p.master]) [s, d].each { |e| expect(Product.in_supplier_or_distributor(e)).to eq([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]) expect(Product.in_order_cycle(oc1)).to eq([p1]) end end describe "in_an_active_order_cycle" do it "shows products in order cycle distribution" do s = create(:supplier_enterprise) d2 = create(:distributor_enterprise) d3 = create(:distributor_enterprise) p1 = create(:product) p2 = create(:product) p3 = create(:product) oc2 = create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.master], orders_open_at: 8.days.ago, orders_close_at: 1.day.ago) oc2 = create(:simple_order_cycle, suppliers: [s], distributors: [d3], variants: [p3.master], orders_close_at: Date.tomorrow) expect(Product.in_an_active_order_cycle).to eq([p3]) 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 expect(product.count).to eq(1) expect(product).to include @p1 end it "shows all products for admin user" do user = create(:admin_user) product = Product.managed_by user expect(product.count).to eq(2) expect(product).to include @p1 expect(product).to include @p2 end end describe "visible_for" 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 ) } let!(:products) { Spree::Product.visible_for(enterprise) } it "lists any products with variants that are listed as visible=true" do expect(products).to include visible_variant.product expect(products).to_not include new_variant.product, hidden_variant.product 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!(:p1) { create(:simple_product, supplier: shop ) } let!(:p2) { create(:simple_product, supplier: add_to_oc_producer ) } let!(:p3) { 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 products produced by the enterprise and any producers granting P-OC' do stockable_products = Spree::Product.stockable_by(shop) expect(stockable_products).to include p1, p2 expect(stockable_products).to_not include p3 end end end describe "properties" do it "returns product properties as a hash" do product = create(:simple_product) product.set_property 'Organic Certified', 'NASAA 12345' property = product.properties.last expect(product.properties_including_inherited).to eq([{ id: property.id, name: "Organic Certified", value: 'NASAA 12345' }]) end it "returns producer properties as a hash" do supplier = create(:supplier_enterprise) product = create(:simple_product, supplier: supplier) supplier.set_producer_property 'Organic Certified', 'NASAA 54321' property = supplier.properties.last expect(product.properties_including_inherited).to eq([{ id: property.id, name: "Organic Certified", value: 'NASAA 54321' }]) end it "overrides producer properties with product properties" do supplier = create(:supplier_enterprise) product = create(:simple_product, supplier: supplier) product.set_property 'Organic Certified', 'NASAA 12345' supplier.set_producer_property 'Organic Certified', 'NASAA 54321' property = product.properties.last expect(product.properties_including_inherited).to eq([{ id: property.id, name: "Organic Certified", value: 'NASAA 12345' }]) end context "when product has an inherit_properties value set to true" do let(:supplier) { create(:supplier_enterprise) } let(:product) { create(:simple_product, supplier: supplier, inherits_properties: true) } it "inherits producer properties" do supplier.set_producer_property 'Organic Certified', 'NASAA 54321' property = supplier.properties.last expect(product.properties_including_inherited).to eq([{ id: property.id, name: "Organic Certified", value: 'NASAA 54321' }]) end end context "when product has an inherit_properties value set to false" do let(:supplier) { create(:supplier_enterprise) } let(:product) { create(:simple_product, supplier: supplier, inherits_properties: false) } it "does not inherit producer properties" do supplier.set_producer_property 'Organic Certified', 'NASAA 54321' expect(product.properties_including_inherited).to eq([]) end end it "sorts by position" do supplier = create(:supplier_enterprise) product = create(:simple_product, supplier: supplier) pa = Spree::Property.create! name: 'A', presentation: 'A' pb = Spree::Property.create! name: 'B', presentation: 'B' pc = Spree::Property.create! name: 'C', presentation: 'C' product.product_properties.create!({ property_id: pa.id, value: '1', position: 1 }, without_protection: true) product.product_properties.create!({ property_id: pc.id, value: '3', position: 3 }, without_protection: true) supplier.producer_properties.create!({ property_id: pb.id, value: '2', position: 2 }, without_protection: true) expect(product.properties_including_inherited).to eq( [{ id: pa.id, name: "A", value: '1' }, { id: pb.id, name: "B", value: '2' }, { id: pc.id, name: "C", value: '3' }] ) end end describe "membership" do 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(:simple_order_cycle, distributors: [d1], variants: [p1.master]) oc2 = create(:simple_order_cycle, distributors: [d2], variants: [p2.master]) expect(p1).to be_in_distributor d1 expect(p1).not_to 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(:simple_order_cycle, distributors: [d1], variants: [p1.master]) oc2 = create(:simple_order_cycle, distributors: [d2], variants: [p2.master]) expect(p1).to be_in_order_cycle oc1 expect(p1).not_to be_in_order_cycle oc2 end end describe "variant units" do 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) expect(p.option_types).to eq([ot_volume]) end it "does not remove and re-add the option type if it is not changed" do expect(p.option_types).to receive(:delete).never p.update_attributes!(name: 'foo') end it "removes the related option values from all its variants and replaces them" do ot = Spree::OptionType.find_by_name 'unit_weight' v = create(:variant, unit_value: 1, product: p) p.reload expect(v.option_values.map(&:name).include?("1L")).to eq(false) expect(v.option_values.map(&:name).include?("1g")).to eq(true) expect { p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 0.001) }.to change(p.master.option_values(true), :count).by(0) v.reload expect(v.option_values.map(&:name).include?("1L")).to eq(true) expect(v.option_values.map(&:name).include?("1g")).to eq(false) end it "removes the related option values from its master variant and replaces them" do ot = Spree::OptionType.find_by_name 'unit_weight' p.master.update_attributes!(unit_value: 1) p.reload expect(p.master.option_values.map(&:name).include?("1L")).to eq(false) expect(p.master.option_values.map(&:name).include?("1g")).to eq(true) expect { p.update_attributes!(variant_unit: 'volume', variant_unit_scale: 0.001) }.to change(p.master.option_values(true), :count).by(0) p.reload expect(p.master.option_values.map(&:name).include?("1L")).to eq(true) expect(p.master.option_values.map(&:name).include?("1g")).to eq(false) 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') expect(Spree::Product.all_variant_unit_option_types).to match_array [ot1, ot2, ot3] 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 expect(v1.option_values(true)).not_to include ov1 expect(v2.option_values(true)).not_to include ov2 # And the option values themselves should still exist expect(Spree::OptionValue.where(id: [ov1.id, ov2.id]).count).to eq(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(:on_hand, 0) expect(product.has_stock?).to eq(true) end describe "finding products in stock for a particular distribution" do it "returns on-demand products" do p = create(:simple_product, on_demand: true) p.variants.first.update_attributes!(on_hand: 0, on_demand: true) d = create(:distributor_enterprise) oc = create(:simple_order_cycle, distributors: [d]) oc.exchanges.outgoing.first.variants << p.variants.first expect(p).to 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(:on_hand, 1) d = create(:distributor_enterprise) oc = create(:simple_order_cycle, distributors: [d]) oc.exchanges.outgoing.first.variants << v expect(p).to 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(:on_hand, 0) d = create(:distributor_enterprise) oc = create(:simple_order_cycle, distributors: [d]) oc.exchanges.outgoing.first.variants << v expect(p).to 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(:on_hand, 1) d = create(:distributor_enterprise) oc = create(:simple_order_cycle, distributors: [d]) expect(p).not_to have_stock_for_distribution(oc, d) end end end describe "taxons" do let(:taxon1) { create(:taxon) } let(:taxon2) { create(:taxon) } let(:product) { create(:simple_product) } it "returns the first taxon as the primary taxon" do expect(product.taxons).to eq([product.primary_taxon]) end end describe "deletion" do let(:p) { create(:simple_product) } let(:v) { create(:variant, product: p) } let(:oc) { create(:simple_order_cycle) } let(:s) { create(:supplier_enterprise) } let(:e) { create(:exchange, order_cycle: oc, incoming: true, sender: s, receiver: oc.coordinator) } it "removes the master variant from all order cycles" do e.variants << p.master p.destroy expect(e.variants(true)).to be_empty end it "removes all other variants from order cycles" do e.variants << v p.destroy expect(e.variants(true)).to be_empty end end end describe "product import" do describe "finding the most recent import date of the variants" do let!(:product) { create(:product) } let(:reference_time) { Time.zone.now.beginning_of_day } before do product.reload end context "when the variants do not have an import date" do let!(:variant_a) { create(:variant, product: product, import_date: nil) } let!(:variant_b) { create(:variant, product: product, import_date: nil) } it "returns nil" do expect(product.import_date).to be_nil end end context "when some variants have import date and some do not" do let!(:variant_a) { create(:variant, product: product, import_date: nil) } let!(:variant_b) { create(:variant, product: product, import_date: reference_time - 1.hour) } let!(:variant_c) { create(:variant, product: product, import_date: reference_time - 2.hours) } it "returns the most recent import date" do expect(product.import_date).to eq(variant_b.import_date) end end context "when all variants have import date" do let!(:variant_a) { create(:variant, product: product, import_date: reference_time - 2.hours) } let!(:variant_b) { create(:variant, product: product, import_date: reference_time - 1.hour) } let!(:variant_c) { create(:variant, product: product, import_date: reference_time - 3.hours) } it "returns the most recent import date" do expect(product.import_date).to eq(variant_b.import_date) end end end end end