# frozen_string_literal: false require 'spec_helper' require 'spree/core/product_duplicator' RSpec.describe Spree::Product do context 'product instance' do let(:product) { create(:product) } context '#duplicate' do it 'duplicates product' do clone = product.duplicate expect(clone).to be_persisted expect(clone.name).to eq "COPY OF #{product.name}" expect(clone.sku).to eq "" expect(clone.image).to eq product.image end it 'fails to duplicate invalid product' do # cloned product will be invalid product.update_columns(name: "l" * 254) expect{ product.duplicate }.to raise_error(ActiveRecord::ActiveRecordError) end end context "product has variants" do before do product.reload.variants << create(:variant, product:) end context "#destroy" do it "should set deleted_at value" do product.destroy expect(product.deleted_at).not_to be_nil expect(product.variants.all? { |v| !v.deleted_at.nil? }).to be_truthy end end end describe 'Variants sorting' do it 'sorts variants by id' do expect(product.variants.to_sql).to match(/ORDER BY spree_variants.id ASC/) end end end context "properties" do let(:product) { create(:product) } it "should properly assign properties" do expect { product.set_property('the_prop', 'value1') product.save product.reload }.to change { product.properties.length }.by(1) expect(product.property('the_prop')).to eq 'value1' product.set_property('the_prop', 'value2') expect(product.property('the_prop')).to eq 'value2' end it "should not create duplicate properties when set_property is called" do product.set_property('the_prop', 'value2') product.save expect { product.set_property('the_prop', 'value2') product.save product.reload }.not_to change { product.properties.length } end # Regression test for #2455 it "should not overwrite properties' presentation names" do Spree::Property.where(name: 'foo').first_or_create!(presentation: "Foo's Presentation Name") product.set_property('foo', 'value1') product.set_property('bar', 'value2') expect(Spree::Property.where(name: 'foo').first.presentation) .to eq "Foo's Presentation Name" expect(Spree::Property.where(name: 'bar').first.presentation).to eq "bar" end end describe "associations" do it { is_expected.to have_one(:image) } it { is_expected.to have_many(:product_properties) } it { is_expected.to have_many(:properties).through(:product_properties) } it { is_expected.to have_many(:variants) } it { is_expected.to have_many(:prices).through(:variants) } it { is_expected.to have_many(:stock_items).through(:variants) } it { is_expected.to have_many(:variant_images).through(:variants) } end describe "validations and defaults" do it "is valid when built from factory" do expect(build(:product)).to be_valid end it { is_expected.to validate_presence_of :name } it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:sku).is_at_most(255) } context "when the product has variants" do let(:product) do product = create(:simple_product) create(:variant, product:) product.reload end it { is_expected.to validate_numericality_of(:price).is_greater_than_or_equal_to(0) } context "saving a new product" do let!(:product){ described_class.new } let!(:shipping_category){ create(:shipping_category) } let!(:taxon){ create(:taxon) } let(:supplier){ create(:enterprise) } it "copies properties to the first standard variant" do product.primary_taxon_id = taxon.id product.name = "Product1" product.variant_unit = "weight" product.variant_unit_scale = 1000 product.unit_value = 1 product.unit_description = "some product" product.price = 4.27 product.shipping_category_id = shipping_category.id product.supplier_id = supplier.id product.save(context: :create_and_create_standard_variant) expect(product.variants.reload.length).to eq 1 standard_variant = product.variants.reload.first expect(standard_variant).to be_valid expect(standard_variant.variant_unit).to eq("weight") expect(standard_variant.variant_unit_scale).to eq(1000) expect(standard_variant.unit_value).to eq(1) expect(standard_variant.unit_description).to eq("some product") expect(standard_variant.price).to eq 4.27 expect(standard_variant.shipping_category).to eq shipping_category expect(standard_variant.primary_taxon).to eq taxon expect(standard_variant.supplier).to eq supplier end context "with variant attributes" do it { is_expected.to validate_presence_of(:variant_unit) .on(:create_and_create_standard_variant) } it { is_expected.to validate_presence_of(:supplier_id) .on(:create_and_create_standard_variant) } it { is_expected.to validate_presence_of(:primary_taxon_id) .on(:create_and_create_standard_variant) } describe "unit_value" do subject { build(:simple_product, variant_unit: "items") } it { is_expected.to validate_numericality_of(:unit_value).is_greater_than(0) .on(:create_and_create_standard_variant) } it { is_expected.not_to validate_presence_of(:unit_value) .on(:create_and_create_standard_variant) } ["weight", "volume"].each do |variant_unit| context "when variant_unit is #{variant_unit}" do subject { build(:simple_product, variant_unit:) } it { is_expected.to validate_presence_of(:unit_value) .on(:create_and_create_standard_variant) } end end describe "unit_description" do it { is_expected.not_to validate_presence_of(:unit_description) .on(:create_and_create_standard_variant) } context "when variant_unit is et and unit_value is nil" do subject { build(:simple_product, variant_unit: "items", unit_value: nil, unit_description: "box") } it { is_expected.to validate_presence_of(:unit_description) .on(:create_and_create_standard_variant) } end end describe "variant_unit_scale" do it { is_expected.not_to validate_presence_of(:variant_unit_scale) .on(:create_and_create_standard_variant) } ["weight", "volume"].each do |variant_unit| context "when variant_unit is #{variant_unit}" do subject { build(:simple_product, variant_unit:) } it { is_expected.to validate_presence_of(:variant_unit_scale) .on(:create_and_create_standard_variant) } end end end describe "variant_unit_name" do subject { build(:simple_product, variant_unit: "volume") } it { is_expected.not_to validate_presence_of(:variant_unit_name) .on(:create_and_create_standard_variant) } context "when variant_unit is items" do subject { build(:simple_product, variant_unit: "items") } it { is_expected.to validate_presence_of(:variant_unit_name) .on(:create_and_create_standard_variant) } end end end end end end describe "#validate_image" do subject(:product) { create(:product_with_image) } context "when the image is invalid" do before { allow(product.image).to receive(:valid?).and_return(false) } context "and has been changed" do before { expect(product.image).to receive(:changed?).and_return(true) } it "adds an error message to the base object" do expect(product).not_to be_valid expect(product.errors[:base]).to include('Image attachment is not a valid image.') end end it "ignores if unchanged" do expect(product).to be_valid end end context "when image is valid" do it { is_expected.to be_valid } end context "when image is blank" do subject { create(:product) } it { is_expected.to be_valid } end end end describe "callbacks" do let(:product) { create(:simple_product) } describe "destroy product" do let(:product) { create(:simple_product, supplier_id: distributor.id) } let(:distributor) { create(:distributor_enterprise) } let!(:oc) { create(:simple_order_cycle, distributors: [distributor], variants: [product.variants.first]) } it "removes variants from order cycles" do expect { product.destroy }.to change { ExchangeVariant.count } end end describe "after updating primary taxon" do let(:product) { create(:simple_product, supplier_id: supplier.id) } let(:supplier) { create(:supplier_enterprise) } let(:new_taxon) { create(:taxon) } it "touches the supplier" do expect { product.update(primary_taxon_id: new_taxon.id) } .to change { supplier.reload.updated_at } end context "when product has no variant" do it "doesn't blow up" do product.variants = [] product.save! expect { product.update(primary_taxon_id: new_taxon.id) }.not_to raise_error end end end describe "after touching the product" do let(:product) { create(:simple_product, supplier_id: supplier.id) } let(:supplier) { create(:supplier_enterprise) } it "touches the supplier" do expect { product.touch } .to change { supplier.reload.updated_at } end context "when the first variant is missing supplier" do it "doesn't blow up" do product.variants.first.update_attribute(:supplier_id, nil) expect { product.touch }.not_to raise_error end end end end describe "scopes" do describe ".with_properties" do let!(:product_with_wanted_property) { create(:product, properties: [wanted_property]) } let!(:product_without_wanted_property_property) { create(:product, properties: [unwanted_property]) } let!(:product_ignoring_property) { create(:product, inherits_properties: false) } let(:wanted_property) { create(:property, presentation: 'Certified Organic') } let(:unwanted_property) { create(:property, presentation: 'Latest Hype') } it "returns no products without a property id" do expect(described_class.with_properties([])).to eq [] end it "returns only products with the wanted property set both on supplier & product itself" do expect( described_class.with_properties([wanted_property.id, 99_999]) ).to match_array [product_with_wanted_property] end end describe "in_supplier" do it "shows products in supplier" do s1 = create(:supplier_enterprise) p1 = create(:product, supplier_id: s1.id) # We create two variants to let us test we don't get duplicated product create(:variant, product: p1, supplier: s1) create(:variant, product: p1, supplier: s1) s2 = create(:supplier_enterprise) p2 = create(:product, supplier_id: s2.id) create(:variant, product: p2, supplier: s2) expect(described_class.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.variants.first]) create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.variants.first]) expect(described_class.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(described_class.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.variants.first expect(described_class.in_distributor(d)).to be_empty end end describe "in_distributors" do let!(:distributor1) { create(:distributor_enterprise) } let!(:distributor2) { create(:distributor_enterprise) } let!(:product1) { create(:product) } let!(:product2) { create(:product) } let!(:product3) { create(:product) } let!(:product4) { create(:product) } let!(:order_cycle1) { create(:order_cycle, distributors: [distributor1], variants: [product1.variants.first, product2.variants.first]) } let!(:order_cycle2) { create(:order_cycle, distributors: [distributor2], variants: [product3.variants.first]) } it "returns distributed products for a given Enterprise AR relation" do distributors = Enterprise.where(id: [distributor1.id, distributor2.id]).to_a expect(described_class.in_distributors(distributors)). to include product1, product2, product3 expect(described_class.in_distributors(distributors)).not_to include product4 end it "returns distributed products for a given array of enterprise ids" do distributors_ids = [distributor1.id, distributor2.id] expect(described_class.in_distributors(distributors_ids)). to include product1, product2, product3 expect(described_class.in_distributors(distributors_ids)).not_to include product4 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.variants.first]) oc2 = create(:simple_order_cycle, suppliers: [s], distributors: [d2], variants: [p2.variants.first]) expect(described_class.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.variants.first], orders_open_at: 8.days.ago, orders_close_at: 1.day.ago) oc2 = create(:simple_order_cycle, suppliers: [s], distributors: [d3], variants: [p3.variants.first], orders_close_at: Date.tomorrow) expect(described_class.in_an_active_order_cycle).to eq([p3]) end end describe "by_producer" do it "orders by producer name" do producer_z = create(:enterprise, name: "z_cooperative") producer_a = create(:enterprise, name: "a_cooperative") producer_g = create(:enterprise, name: "g_cooperative") product1 = create(:product, supplier_id: producer_z.id) product2 = create(:product, supplier_id: producer_a.id) product3 = create(:product, supplier_id: producer_g.id) expect(described_class.by_producer).to eq([product2, product3, product1]) end end describe "managed_by" do let!(:e1) { create(:enterprise) } let!(:e2) { create(:enterprise) } let!(:p1) { create(:product) } let!(:p2) { create(:product) } before(:each) do create(:variant, product: p1, supplier: e1) create(:variant, product: p1, supplier: e2) end it "shows only products for given user" do user = create(:user) e1.enterprise_roles.build(user:).save product = described_class.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 = described_class.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!(:product) { create(:product) } let!(:visible_variant1) { create(:variant, product:) } let!(:visible_variant2) { create(:variant, product:) } let!(:hidden_inventory_item) { create(:inventory_item, enterprise:, variant: hidden_variant, visible: false ) } let!(:visible_inventory_item1) { create(:inventory_item, enterprise:, variant: visible_variant1, visible: true ) } let!(:visible_inventory_item2) { create(:inventory_item, enterprise:, variant: visible_variant2, visible: true ) } let!(:products) { described_class.visible_for(enterprise) } it "lists any products with variants that are listed as visible=true" do expect(products.length).to eq(1) expect(products).to include product expect(products).not_to include new_variant.product, hidden_variant.product end end describe "imported_on" do let!(:v1) { create(:variant, import_date: 1.day.ago) } let!(:v2) { create(:variant, import_date: 2.days.ago) } let!(:v3) { create(:variant, import_date: 1.day.ago) } it "returns products imported on given day" do imported_products = described_class.imported_on(1.day.ago.to_date) expect(imported_products).to include v1.product, v3.product end end end describe "#properties_including_inherited" do let(:product) { create(:simple_product) } let(:supplier) { create(:supplier_enterprise) } before do product.variants = [] product.variants << create(:variant, product:, supplier:) end it "returns product properties as a hash" do 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.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 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(:product) { create(:simple_product, 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(:product) { create(:simple_product, 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 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 }) product.product_properties.create!({ property_id: pc.id, value: '3', position: 3 }) supplier.producer_properties.create!({ property_id: pb.id, value: '2', position: 2 }) 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.variants.first]) oc2 = create(:simple_order_cycle, distributors: [d2], variants: [p2.variants.first]) 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.variants.first]) oc2 = create(:simple_order_cycle, distributors: [d2], variants: [p2.variants.first]) expect(p1).to be_in_order_cycle oc1 expect(p1).not_to be_in_order_cycle oc2 end end describe "deletion" do let(:product) { create(:simple_product) } let(:variant) { create(:variant, product:) } let(:order_cycle) { create(:simple_order_cycle) } let(:supplier) { create(:supplier_enterprise) } let(:exchange) { create( :exchange, order_cycle:, incoming: true, sender: supplier, receiver: order_cycle.coordinator ) } it "removes all variants from order cycles" do exchange.variants << variant product.destroy expect(exchange.variants.reload).to be_empty end end describe "serialisation" do it "sanitises HTML in description" do subject.description = "Hello dearest monster." expect(subject.description).to eq "Hello alert dearest monster." end end end RSpec.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:, import_date: nil) } let!(:variant_b) { create(:variant, 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:, import_date: nil) } let!(:variant_b) { create(:variant, product:, import_date: reference_time - 1.hour) } let!(:variant_c) { create(:variant, 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:, import_date: reference_time - 2.hours) } let!(:variant_b) { create(:variant, product:, import_date: reference_time - 1.hour) } let!(:variant_c) { create(:variant, 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