diff --git a/app/controllers/admin/dfc_product_imports_controller.rb b/app/controllers/admin/dfc_product_imports_controller.rb index 0c545f37bf..8b7bb07e10 100644 --- a/app/controllers/admin/dfc_product_imports_controller.rb +++ b/app/controllers/admin/dfc_product_imports_controller.rb @@ -48,9 +48,9 @@ module Admin existing_variant = @enterprise.supplied_variants.linked_to(semantic_id) if existing_variant - SuppliedProductBuilder.update_product(subject, existing_variant) + SuppliedProductImporter.update_product(subject, existing_variant) else - SuppliedProductBuilder.store_product(subject, @enterprise) + SuppliedProductImporter.store_product(subject, @enterprise) end end diff --git a/app/models/spree/product.rb b/app/models/spree/product.rb index 81b1225c25..171f6de744 100755 --- a/app/models/spree/product.rb +++ b/app/models/spree/product.rb @@ -36,6 +36,7 @@ module Spree searchable_scopes :active, :with_properties has_one :image, class_name: "Spree::Image", as: :viewable, dependent: :destroy + has_one :semantic_link, as: :subject, dependent: :delete has_many :product_properties, dependent: :destroy has_many :properties, through: :product_properties diff --git a/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb index c8b2ba59ff..00d0b63da7 100644 --- a/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb +++ b/engines/dfc_provider/app/controllers/dfc_provider/catalog_items_controller.rb @@ -13,13 +13,15 @@ module DfcProvider EnterpriseBuilder.enterprise(enterprise) end person.affiliatedOrganizations = enterprises + catalog_items = enterprises.flat_map(&:catalogItems) render json: DfcIo.export( person, - *person.affiliatedOrganizations, - *person.affiliatedOrganizations.flat_map(&:catalogItems), - *person.affiliatedOrganizations.flat_map(&:catalogItems).map(&:product), - *person.affiliatedOrganizations.flat_map(&:catalogItems).flat_map(&:offers), + *enterprises, + *catalog_items, + *catalog_items.map(&:product), + *catalog_items.map(&:product).flat_map(&:isVariantOf), + *catalog_items.flat_map(&:offers), ) end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/product_groups_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/product_groups_controller.rb new file mode 100644 index 0000000000..35c8702f7f --- /dev/null +++ b/engines/dfc_provider/app/controllers/dfc_provider/product_groups_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Show Spree::Product as SuppliedProduct with variants. +module DfcProvider + class ProductGroupsController < DfcProvider::ApplicationController + def show + spree_product = permissions.visible_products.find(params[:id]) + product = ProductGroupBuilder.product_group(spree_product) + render json: DfcIo.export(product) + end + + private + + def permissions + OpenFoodNetwork::Permissions.new(current_user) + end + end +end diff --git a/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb index 438c9c86a3..51857b80b4 100644 --- a/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb +++ b/engines/dfc_provider/app/controllers/dfc_provider/supplied_products_controller.rb @@ -14,7 +14,7 @@ module DfcProvider return head :bad_request unless supplied_product - variant = SuppliedProductBuilder.store_product( + variant = SuppliedProductImporter.store_product( supplied_product, current_enterprise, ) @@ -33,7 +33,7 @@ module DfcProvider return head :bad_request unless supplied_product - SuppliedProductBuilder.update_product(supplied_product, variant) + SuppliedProductImporter.update_product(supplied_product, variant) end private diff --git a/engines/dfc_provider/app/services/dfc_catalog.rb b/engines/dfc_provider/app/services/dfc_catalog.rb index 08649494c7..431454bd54 100644 --- a/engines/dfc_provider/app/services/dfc_catalog.rb +++ b/engines/dfc_provider/app/services/dfc_catalog.rb @@ -13,9 +13,13 @@ class DfcCatalog @graph = graph end + # List all products in this catalog. + # These are SuppliedProduct objects which may be grouped as variants. + # But we don't return the parent products having variants. def products @products ||= @graph.select do |subject| - subject.is_a? DataFoodConsortium::Connector::SuppliedProduct + subject.is_a?(DataFoodConsortium::Connector::SuppliedProduct) && + subject.variants.blank? end end diff --git a/engines/dfc_provider/app/services/product_group_builder.rb b/engines/dfc_provider/app/services/product_group_builder.rb new file mode 100644 index 0000000000..c8c8e6f62d --- /dev/null +++ b/engines/dfc_provider/app/services/product_group_builder.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class ProductGroupBuilder < DfcBuilder + def self.product_group(product) + id = urls.product_group_url(id: product.id) + variants = product.variants.map do |spree_variant| + SuppliedProductBuilder.semantic_id(spree_variant) + end + + DataFoodConsortium::Connector::SuppliedProduct.new( + id, variants:, + name: product.name, + ) + end + + def self.apply(supplied_product, spree_product) + description = supplied_product.isVariantOf.first.try(:description) || + supplied_product.description + name = supplied_product.isVariantOf.first.try(:name) + spree_product.description = description if description.present? + spree_product.name = name if name.present? + end +end diff --git a/engines/dfc_provider/app/services/supplied_product_builder.rb b/engines/dfc_provider/app/services/supplied_product_builder.rb index ff567ff52b..d2d1751be2 100644 --- a/engines/dfc_provider/app/services/supplied_product_builder.rb +++ b/engines/dfc_provider/app/services/supplied_product_builder.rb @@ -1,118 +1,38 @@ # frozen_string_literal: true class SuppliedProductBuilder < DfcBuilder - def self.supplied_product(variant) - id = urls.enterprise_supplied_product_url( + def self.semantic_id(variant) + urls.enterprise_supplied_product_url( enterprise_id: variant.supplier_id, id: variant.id, ) + end + + def self.supplied_product(variant) product_uri = urls.enterprise_url( variant.supplier_id, spree_product_id: variant.product_id ) + product_group = ProductGroupBuilder.product_group(variant.product) DfcProvider::SuppliedProduct.new( - id, + semantic_id(variant), name: variant.product_and_full_name, description: variant.description, productType: product_type(variant), quantity: QuantitativeValueBuilder.quantity(variant), + isVariantOf: [product_group], spree_product_uri: product_uri, spree_product_id: variant.product.id, image_url: variant.product&.image&.url(:product) ) end - def self.store_product(subject, enterprise) - return unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct - - variant = SuppliedProductBuilder.import_variant(subject, enterprise) - product = variant.product - - product.save! if product.new_record? - variant.save! if variant.new_record? - - variant - end - - def self.update_product(supplied_product, variant) - apply(supplied_product, variant) - - variant.product.save! - variant.save! - - variant - end - - def self.import_variant(supplied_product, supplier) - product = referenced_spree_product(supplied_product, supplier) - - if product - Spree::Variant.new( product:, supplier:, price: 0,).tap do |variant| - apply(supplied_product, variant) - end - else - product = import_product(supplied_product, supplier) - product.variants.first.tap { |variant| apply(supplied_product, variant) } - end.tap do |variant| - link = supplied_product.semanticId - variant.semantic_links.new(semantic_id: link) if link.present? - end - end - - def self.referenced_spree_product(supplied_product, supplier) - uri = supplied_product.spree_product_uri - id = supplied_product.spree_product_id - - if uri.present? - route = Rails.application.routes.recognize_path(uri) - params = Rack::Utils.parse_nested_query(URI.parse(uri).query) - - # Check that the given URI points to us: - return unless uri == urls.enterprise_url(route.merge(params)) - - supplier.supplied_products.find_by(id: params["spree_product_id"]) - elsif id.present? - supplier.supplied_products.find_by(id:) - end - end - - def self.import_product(supplied_product, supplier) - Spree::Product.new( - name: supplied_product.name, - description: supplied_product.description, - price: 0, # will be in DFC Offer - supplier_id: supplier.id, - primary_taxon_id: taxon(supplied_product).id, - image: ImageBuilder.import(supplied_product.image), - ).tap do |product| - QuantitativeValueBuilder.apply(supplied_product.quantity, product) - product.ensure_standard_variant - end - end - - def self.apply(supplied_product, variant) - variant.product.assign_attributes(description: supplied_product.description) - - variant.display_name = supplied_product.name - variant.primary_taxon = taxon(supplied_product) - QuantitativeValueBuilder.apply(supplied_product.quantity, variant) - - catalog_item = supplied_product&.catalogItems&.first - offer = catalog_item&.offers&.first - CatalogItemBuilder.apply_stock(catalog_item, variant) - OfferBuilder.apply(offer, variant) - end - def self.product_type(variant) taxon_dfc_id = variant.primary_taxon&.dfc_id DataFoodConsortium::Connector::SKOSParser.concepts[taxon_dfc_id] end - def self.taxon(supplied_product) - ProductTypeImporter.taxon(supplied_product.productType) - end - - private_class_method :product_type, :taxon + private_class_method :product_type end diff --git a/engines/dfc_provider/app/services/supplied_product_importer.rb b/engines/dfc_provider/app/services/supplied_product_importer.rb new file mode 100644 index 0000000000..c524741596 --- /dev/null +++ b/engines/dfc_provider/app/services/supplied_product_importer.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +class SuppliedProductImporter < DfcBuilder + def self.store_product(subject, enterprise) + return unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct + + variant = import_variant(subject, enterprise) + product = variant.product + + product.save! if product.new_record? + variant.save! if variant.new_record? + + variant + end + + def self.update_product(supplied_product, variant) + apply(supplied_product, variant) + + variant.product.save! + variant.save! + + variant + end + + def self.import_variant(supplied_product, supplier) + product = referenced_spree_product(supplied_product, supplier) + + if product + Spree::Variant.new( product:, supplier:, price: 0,).tap do |variant| + apply(supplied_product, variant) + end + else + product = import_product(supplied_product, supplier) + product.variants.first.tap { |variant| apply(supplied_product, variant) } + end.tap do |variant| + link = supplied_product.semanticId + variant.semantic_links.new(semantic_id: link) if link.present? + end + end + + # DEPRECATION WARNING + # Reference by custom `ofn:spree_product_id` and `ofn:spree_product_uri` + # properties is now replaced by the official `dfc-b:isVariantOf`. + # We will remove the old methods at some point. + def self.referenced_spree_product(supplied_product, supplier) + spree_product(supplied_product, supplier) || + spree_product_linked(supplied_product, supplier) || + spree_product_from_uri(supplied_product, supplier) || + spree_product_from_id(supplied_product, supplier) + end + + def self.spree_product(supplied_product, supplier) + supplied_product.isVariantOf.lazy.map do |group| + # We may have an object or just the id here: + group_id = group.try(:semanticId) || group + + id = begin + route = Rails.application.routes.recognize_path(group_id) + + # Check that the given URI points to us: + next if group_id != urls.product_group_url(route) + + route[:id] + rescue ActionController::RoutingError + next + end + + supplier.supplied_products.find_by(id:) + end.compact.first + end + + def self.spree_product_linked(supplied_product, supplier) + semantic_ids = supplied_product.isVariantOf.map do |id_or_object| + id_or_object.try(:semanticId) || id_or_object + end + supplier.supplied_products.includes(:semantic_link) + .where(semantic_link: { semantic_id: semantic_ids }) + .first + end + + def self.spree_product_from_uri(supplied_product, supplier) + uri = supplied_product.spree_product_uri + return if uri.blank? + + route = Rails.application.routes.recognize_path(uri) + params = Rack::Utils.parse_nested_query(URI.parse(uri).query) + + # Check that the given URI points to us: + return unless uri == urls.enterprise_url(route.merge(params)) + + supplier.supplied_products.find_by(id: params["spree_product_id"]) + end + + def self.spree_product_from_id(supplied_product, supplier) + id = supplied_product.spree_product_id + supplier.supplied_products.find_by(id:) if id.present? + end + + def self.import_product(supplied_product, supplier) + Spree::Product.new( + name: supplied_product.name, + description: supplied_product.description, + price: 0, # will be in DFC Offer + supplier_id: supplier.id, + primary_taxon_id: taxon(supplied_product).id, + image: ImageBuilder.import(supplied_product.image), + semantic_link: semantic_link(supplied_product), + ).tap do |product| + QuantitativeValueBuilder.apply(supplied_product.quantity, product) + product.ensure_standard_variant + end + end + + def self.apply(supplied_product, variant) + ProductGroupBuilder.apply(supplied_product, variant.product) + + variant.display_name = supplied_product.name + variant.primary_taxon = taxon(supplied_product) + QuantitativeValueBuilder.apply(supplied_product.quantity, variant) + + catalog_item = supplied_product&.catalogItems&.first + offer = catalog_item&.offers&.first + CatalogItemBuilder.apply_stock(catalog_item, variant) + OfferBuilder.apply(offer, variant) + end + + def self.semantic_link(supplied_product) + group = supplied_product.isVariantOf.first + semantic_id = group.try(:semanticId) || semantic_id + + SemanticLink.new(semantic_id:) if semantic_id.present? + end + + def self.taxon(supplied_product) + ProductTypeImporter.taxon(supplied_product.productType) + end + private_class_method :taxon +end diff --git a/engines/dfc_provider/config/routes.rb b/engines/dfc_provider/config/routes.rb index 8fc0850729..647094b96b 100644 --- a/engines/dfc_provider/config/routes.rb +++ b/engines/dfc_provider/config/routes.rb @@ -12,6 +12,7 @@ DfcProvider::Engine.routes.draw do resources :affiliated_by, only: [:create, :destroy], module: 'enterprise_groups' end resources :persons, only: [:show] + resources :product_groups, only: [:show] resource :affiliate_sales_data, only: [:show] end diff --git a/engines/dfc_provider/spec/requests/addresses_spec.rb b/engines/dfc_provider/spec/requests/addresses_spec.rb index 9e8693b03c..7c06914078 100644 --- a/engines/dfc_provider/spec/requests/addresses_spec.rb +++ b/engines/dfc_provider/spec/requests/addresses_spec.rb @@ -2,7 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "Addresses", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do +RSpec.describe "Addresses", type: :request, swagger_doc: "dfc.yaml" do let(:user) { create(:oidc_user) } let(:address) { create(:address, id: 40_000) } let(:result) { json_response } diff --git a/engines/dfc_provider/spec/requests/affiliate_sales_data_spec.rb b/engines/dfc_provider/spec/requests/affiliate_sales_data_spec.rb index e3ca35e8ad..6e736fbaae 100644 --- a/engines/dfc_provider/spec/requests/affiliate_sales_data_spec.rb +++ b/engines/dfc_provider/spec/requests/affiliate_sales_data_spec.rb @@ -2,7 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "AffiliateSalesData", swagger_doc: "dfc.yaml", rswag_autodoc: true do +RSpec.describe "AffiliateSalesData", swagger_doc: "dfc.yaml" do let(:user) { create(:oidc_user) } before { login_as user } diff --git a/engines/dfc_provider/spec/requests/catalog_items_spec.rb b/engines/dfc_provider/spec/requests/catalog_items_spec.rb index 03e1ba468a..a48f6c0eba 100644 --- a/engines/dfc_provider/spec/requests/catalog_items_spec.rb +++ b/engines/dfc_provider/spec/requests/catalog_items_spec.rb @@ -2,8 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "CatalogItems", type: :request, swagger_doc: "dfc.yaml", - rswag_autodoc: true do +RSpec.describe "CatalogItems", type: :request, swagger_doc: "dfc.yaml" do let(:user) { create(:oidc_user, id: 12_345) } let(:enterprise) { create( diff --git a/engines/dfc_provider/spec/requests/enterprise_groups/affiliated_by_spec.rb b/engines/dfc_provider/spec/requests/enterprise_groups/affiliated_by_spec.rb index fdee727fa8..e6ef549213 100644 --- a/engines/dfc_provider/spec/requests/enterprise_groups/affiliated_by_spec.rb +++ b/engines/dfc_provider/spec/requests/enterprise_groups/affiliated_by_spec.rb @@ -2,8 +2,7 @@ require_relative "../../swagger_helper" -RSpec.describe "EnterpriseGroups::AffiliatedBy", type: :request, swagger_doc: "dfc.yaml", - rswag_autodoc: true do +RSpec.describe "EnterpriseGroups::AffiliatedBy", type: :request, swagger_doc: "dfc.yaml" do let(:user) { create(:oidc_user, id: 12_345) } let(:group) { create( diff --git a/engines/dfc_provider/spec/requests/enterprise_groups_spec.rb b/engines/dfc_provider/spec/requests/enterprise_groups_spec.rb index 8b3f161893..4836a689f5 100644 --- a/engines/dfc_provider/spec/requests/enterprise_groups_spec.rb +++ b/engines/dfc_provider/spec/requests/enterprise_groups_spec.rb @@ -2,7 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "EnterpriseGroups", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do +RSpec.describe "EnterpriseGroups", type: :request, swagger_doc: "dfc.yaml" do let(:user) { create(:oidc_user, id: 12_345) } let(:group) { create( diff --git a/engines/dfc_provider/spec/requests/enterprises_spec.rb b/engines/dfc_provider/spec/requests/enterprises_spec.rb index 13c9f249be..254d04fc11 100644 --- a/engines/dfc_provider/spec/requests/enterprises_spec.rb +++ b/engines/dfc_provider/spec/requests/enterprises_spec.rb @@ -2,7 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "Enterprises", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do +RSpec.describe "Enterprises", type: :request, swagger_doc: "dfc.yaml" do let!(:user) { create(:oidc_user) } let!(:enterprise) do create( diff --git a/engines/dfc_provider/spec/requests/offers_spec.rb b/engines/dfc_provider/spec/requests/offers_spec.rb index 20c0780489..8e3ffe8dab 100644 --- a/engines/dfc_provider/spec/requests/offers_spec.rb +++ b/engines/dfc_provider/spec/requests/offers_spec.rb @@ -2,7 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "Offers", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do +RSpec.describe "Offers", type: :request, swagger_doc: "dfc.yaml" do let!(:user) { create(:oidc_user) } let!(:enterprise) { create(:distributor_enterprise, id: 10_000, owner: user) } let!(:product) { diff --git a/engines/dfc_provider/spec/requests/persons_spec.rb b/engines/dfc_provider/spec/requests/persons_spec.rb index a7ef52c0c4..c6379affd7 100644 --- a/engines/dfc_provider/spec/requests/persons_spec.rb +++ b/engines/dfc_provider/spec/requests/persons_spec.rb @@ -2,7 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "Persons", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do +RSpec.describe "Persons", type: :request, swagger_doc: "dfc.yaml" do let(:user) { create(:oidc_user, id: 10_000) } let(:other_user) { create(:oidc_user) } diff --git a/engines/dfc_provider/spec/requests/product_groups_spec.rb b/engines/dfc_provider/spec/requests/product_groups_spec.rb new file mode 100644 index 0000000000..215bfb3733 --- /dev/null +++ b/engines/dfc_provider/spec/requests/product_groups_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require_relative "../swagger_helper" + +RSpec.describe "ProductGroups", swagger_doc: "dfc.yaml" do + let!(:user) { create(:oidc_user) } + let!(:enterprise) { create(:distributor_enterprise, id: 10_000, owner: user) } + let!(:product) { + create( + :product_with_image, + id: 90_000, + name: "Pesto", description: "Basil Pesto", + variants: [variant] + ) + } + let(:variant) { + build(:base_variant, id: 10_001, unit_value: 1, primary_taxon: taxon, supplier: enterprise) + } + let(:taxon) { + build( + :taxon, + name: "Processed Vegetable", + dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#processed-vegetable" + ) + } + + before { login_as user } + + path "/api/dfc/product_groups/{id}" do + parameter name: :enterprise_id, in: :path, type: :string + parameter name: :id, in: :path, type: :string + + let(:enterprise_id) { enterprise.id } + + get "Show ProductGroup" do + produces "application/json" + + response "200", "success" do + let(:id) { product.id } + + run_test! do + expect(json_response["@id"]).to eq "http://test.host/api/dfc/product_groups/90000" + + expect(json_response["dfc-b:hasVariant"]).to eq "http://test.host/api/dfc/enterprises/10000/supplied_products/10001" + end + end + end + end +end diff --git a/engines/dfc_provider/spec/requests/social_medias_spec.rb b/engines/dfc_provider/spec/requests/social_medias_spec.rb index 71e7bfdbee..32c479f593 100644 --- a/engines/dfc_provider/spec/requests/social_medias_spec.rb +++ b/engines/dfc_provider/spec/requests/social_medias_spec.rb @@ -2,7 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "SocialMedias", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do +RSpec.describe "SocialMedias", type: :request, swagger_doc: "dfc.yaml" do let(:user) { create(:oidc_user) } let(:enterprise) do create( diff --git a/engines/dfc_provider/spec/requests/supplied_products_spec.rb b/engines/dfc_provider/spec/requests/supplied_products_spec.rb index cb707b3fdb..a5f7d857ab 100644 --- a/engines/dfc_provider/spec/requests/supplied_products_spec.rb +++ b/engines/dfc_provider/spec/requests/supplied_products_spec.rb @@ -2,7 +2,7 @@ require_relative "../swagger_helper" -RSpec.describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do +RSpec.describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml" do let!(:user) { create(:oidc_user) } let!(:enterprise) { create(:distributor_enterprise, id: 10_000, owner: user) } let!(:product) { @@ -191,6 +191,7 @@ RSpec.describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswa run_test! do expect(response.body).to include variant.name + expect(json_response["dfc-b:isVariantOf"]).to eq "http://test.host/api/dfc/product_groups/90000" expect(json_response["ofn:spree_product_id"]).to eq 90_000 expect(json_response["dfc-b:hasType"]).to eq("dfc-pt:processed-vegetable") expect(json_response["ofn:image"]).to include("logo-white.png") diff --git a/engines/dfc_provider/spec/services/supplied_product_builder_spec.rb b/engines/dfc_provider/spec/services/supplied_product_builder_spec.rb index bd222af048..e76523ecb4 100644 --- a/engines/dfc_provider/spec/services/supplied_product_builder_spec.rb +++ b/engines/dfc_provider/spec/services/supplied_product_builder_spec.rb @@ -83,272 +83,4 @@ RSpec.describe SuppliedProductBuilder do ) end end - - describe ".store_product" do - let(:subject) { builder.store_product(product, supplier) } - let(:product) { - DfcIo.import(product_json).find do |subject| - subject.is_a? DataFoodConsortium::Connector::SuppliedProduct - end - } - let(:product_json) { ExampleJson.read("product.GET") } - - before do - taxon.save! - end - - it "stores a new Spree Product and Variant" do - expect { subject }.to change { - Spree::Product.count - }.by(1) - - expect(subject).to be_a(Spree::Variant) - expect(subject).to be_valid - expect(subject).to be_persisted - expect(subject.name).to eq("Fillet Steak - 201g x 1 Steak") - expect(subject.variant_unit).to eq("items") - expect(subject.variant_unit_scale).to eq(nil) - expect(subject.variant_unit_with_scale).to eq("items") - expect(subject.unit_value).to eq(1) - end - end - - describe ".update_product" do - let(:subject) { builder.update_product(product, variant) } - let(:product) { - DfcIo.import(product_json).find do |subject| - subject.is_a? DataFoodConsortium::Connector::SuppliedProduct - end - } - let(:product_json) { ExampleJson.read("product.GET") } - - it "updates a variant" do - variant # Create test data first - - expect { subject }.not_to change { - Spree::Variant.count - } - - expect(subject).to eq variant - expect(subject.display_name).to eq "Fillet Steak - 201g x 1 Steak" - expect(subject.variant_unit).to eq "items" - expect(subject.unit_value).to eq 1 - expect(subject.on_demand).to eq false - expect(subject.on_hand).to eq 11 - end - end - - describe ".import_product" do - let(:supplied_product) do - DfcProvider::SuppliedProduct.new( - "https://example.net/tomato", - name: "Tomato", - description: "Awesome tomato", - quantity: DataFoodConsortium::Connector::QuantitativeValue.new( - unit: DfcLoader.connector.MEASURES.KILOGRAM, - value: 2, - ), - productType: product_type, - image: "https://cd.net/tomato.png?v=5", - ) - end - let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE } - let!(:taxon) { - create( - :taxon, - name: "Non local vegetable", - dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#non-local-vegetable" - ) - } - - before do - stub_request(:get, "https://cd.net/tomato.png?v=5").to_return( - status: 200, - body: black_logo_path.read, - ) - end - - it "creates a new Spree::Product" do - product = builder.import_product(supplied_product, supplier) - - expect(product).to be_a(Spree::Product) - expect(product.name).to eq("Tomato") - expect(product.description).to eq("Awesome tomato") - expect(product.variant_unit).to eq("weight") - expect(product.image).to be_present - expect(product.image.attachment).to be_attached - expect(product.image.url(:product)).to match /^http.*tomato\.png/ - end - - describe "taxon" do - it "assigns the taxon matching the DFC product type" do - product = builder.import_product(supplied_product, supplier) - - expect(product.variants.first.primary_taxon).to eq(taxon) - end - end - end - - describe ".import_variant" do - let(:imported_variant) { builder.import_variant(supplied_product, supplier) } - let(:supplied_product) do - DfcProvider::SuppliedProduct.new( - "https://example.net/tomato", - name: "Tomato", - description: "Awesome tomato", - quantity: DataFoodConsortium::Connector::QuantitativeValue.new( - unit: DfcLoader.connector.MEASURES.KILOGRAM, - value: 2, - ), - productType: product_type, - catalogItems: [catalog_item], - ) - end - let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE } - let(:catalog_item) { - DataFoodConsortium::Connector::CatalogItem.new( - nil, - # On-demand is expressed as negative stock. - # And some APIs send strings instead of numbers... - stockLimitation: "-1", - offers: [offer], - ) - } - let(:offer) { - DataFoodConsortium::Connector::Offer.new( - nil, - price: DataFoodConsortium::Connector::Price.new(value: "15.50"), - ) - } - - it "creates a new Spree::Product and variant" do - create(:taxon) - - expect(imported_variant).to be_a(Spree::Variant) - expect(imported_variant).to be_valid - expect(imported_variant.id).to be_nil - expect(imported_variant.semantic_links.size).to eq 1 - - link = imported_variant.semantic_links[0] - expect(link.semantic_id).to eq "https://example.net/tomato" - - imported_product = imported_variant.product - expect(imported_product).to be_a(Spree::Product) - expect(imported_product).to be_valid - expect(imported_product.id).to be_nil - expect(imported_product.name).to eq("Tomato") - expect(imported_product.description).to eq("Awesome tomato") - expect(imported_product.variant_unit).to eq("weight") - - # Stock can only be checked when persisted: - imported_product.save! - expect(imported_variant.price).to eq 15.50 - expect(imported_variant.on_demand).to eq true - expect(imported_variant.on_hand).to eq 0 - end - - context "with spree_product_id supplied" do - let(:imported_variant) { builder.import_variant(supplied_product, supplier) } - - let(:supplied_product) do - DfcProvider::SuppliedProduct.new( - "https://example.net/tomato", - name: "Tomato", - description: "Better Awesome tomato", - quantity: DataFoodConsortium::Connector::QuantitativeValue.new( - unit: DfcLoader.connector.MEASURES.KILOGRAM, - value: 2, - ), - productType: product_type, - spree_product_id: variant.product.id - ) - end - let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.DRINK.SOFT_DRINK.FRUIT_JUICE } - let!(:new_taxon) { - create( - :taxon, - name: "Fruit Juice", - dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#fruit-juice" - ) - } - - it "update an existing Spree::Product" do - imported_product = imported_variant.product - expect(imported_product.id).to eq(spree_product.id) - expect(imported_product.description).to eq("Better Awesome tomato") - expect(imported_variant.primary_taxon).to eq(new_taxon) - end - - context "when spree_product_uri doesn't match the server host" do - let(:supplied_product) do - DfcProvider::SuppliedProduct.new( - "https://example.net/tomato", - name: "Tomato", - description: "Awesome tomato", - quantity: DataFoodConsortium::Connector::QuantitativeValue.new( - unit: DfcLoader.connector.MEASURES.KILOGRAM, - value: 2, - ), - productType: product_type, - spree_product_uri: "http://another.host/api/dfc/enterprises/10/supplied_products/50" - ) - end - - it "creates a new Spree::Product and variant" do - expect(imported_variant).to be_a(Spree::Variant) - expect(imported_variant.id).to be_nil - - imported_product = imported_variant.product - expect(imported_product).to be_a(Spree::Product) - expect(imported_product.id).to be_nil - expect(imported_product.name).to eq("Tomato") - expect(imported_product.description).to eq("Awesome tomato") - expect(imported_product.variant_unit).to eq("weight") - end - end - end - end - - describe ".referenced_spree_product" do - let(:result) { builder.referenced_spree_product(supplied_product, supplier) } - let(:supplied_product) do - DfcProvider::SuppliedProduct.new( - "https://example.net/tomato", - name: "Tomato", - ) - end - - it "returns nil when no reference is given" do - expect(result).to eq nil - end - - it "returns a product referenced by URI" do - variant.save! - supplied_product.spree_product_uri = - "http://test.host/api/dfc/enterprises/7?spree_product_id=6" - expect(result).to eq spree_product - end - - it "doesn't return a product of another enterprise" do - variant.save! - create(:product, id: 8, supplier_id: create(:enterprise).id) - - supplied_product.spree_product_uri = - "http://test.host/api/dfc/enterprises/7?spree_product_id=8" - expect(result).to eq nil - end - - it "doesn't return a foreign product referenced by URI" do - variant.save! - supplied_product.spree_product_uri = - "http://another.host/api/dfc/enterprises/7?spree_product_id=6" - expect(result).to eq nil - end - - it "returns a product referenced by id" do - variant.save! - supplied_product.spree_product_id = "6" - expect(result).to eq spree_product - end - end end diff --git a/engines/dfc_provider/spec/services/supplied_product_importer_spec.rb b/engines/dfc_provider/spec/services/supplied_product_importer_spec.rb new file mode 100644 index 0000000000..d344f1653b --- /dev/null +++ b/engines/dfc_provider/spec/services/supplied_product_importer_spec.rb @@ -0,0 +1,332 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe SuppliedProductImporter do + include FileHelper + + subject(:importer) { described_class } + let(:variant) { + create(:variant, id: 5, product: spree_product, primary_taxon: taxon, supplier:) + } + let(:spree_product) { + create(:product, id: 6) + } + let(:supplier) { + create(:supplier_enterprise, id: 7) + } + let(:taxon) { + build( + :taxon, + name: "Soft Drink", + dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#soft-drink" + ) + } + + describe ".store_product" do + let(:subject) { importer.store_product(product, supplier) } + let(:product) { + DfcIo.import(product_json).find do |subject| + subject.is_a? DataFoodConsortium::Connector::SuppliedProduct + end + } + let(:product_json) { ExampleJson.read("product.GET") } + + before do + taxon.save! + end + + it "stores a new Spree Product and Variant" do + expect { subject }.to change { + Spree::Product.count + }.by(1) + + expect(subject).to be_a(Spree::Variant) + expect(subject).to be_valid + expect(subject).to be_persisted + expect(subject.name).to eq("Fillet Steak - 201g x 1 Steak") + expect(subject.variant_unit).to eq("items") + expect(subject.variant_unit_scale).to eq(nil) + expect(subject.variant_unit_with_scale).to eq("items") + expect(subject.unit_value).to eq(1) + end + end + + describe ".update_product" do + let(:subject) { importer.update_product(product, variant) } + let(:product) { + DfcIo.import(product_json).find do |subject| + subject.is_a? DataFoodConsortium::Connector::SuppliedProduct + end + } + let(:product_json) { ExampleJson.read("product.GET") } + + it "updates a variant" do + variant # Create test data first + + expect { subject }.not_to change { + Spree::Variant.count + } + + expect(subject).to eq variant + expect(subject.display_name).to eq "Fillet Steak - 201g x 1 Steak" + expect(subject.variant_unit).to eq "items" + expect(subject.unit_value).to eq 1 + expect(subject.on_demand).to eq false + expect(subject.on_hand).to eq 11 + end + end + + describe ".import_product" do + let(:supplied_product) do + DfcProvider::SuppliedProduct.new( + "https://example.net/tomato", + name: "Tomato", + description: "Awesome tomato", + quantity: DataFoodConsortium::Connector::QuantitativeValue.new( + unit: DfcLoader.connector.MEASURES.KILOGRAM, + value: 2, + ), + productType: product_type, + image: "https://cd.net/tomato.png?v=5", + ) + end + let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE } + let!(:taxon) { + create( + :taxon, + name: "Non local vegetable", + dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#non-local-vegetable" + ) + } + + before do + stub_request(:get, "https://cd.net/tomato.png?v=5").to_return( + status: 200, + body: black_logo_path.read, + ) + end + + it "creates a new Spree::Product" do + product = importer.import_product(supplied_product, supplier) + + expect(product).to be_a(Spree::Product) + expect(product.name).to eq("Tomato") + expect(product.description).to eq("Awesome tomato") + expect(product.variant_unit).to eq("weight") + expect(product.image).to be_present + expect(product.image.attachment).to be_attached + expect(product.image.url(:product)).to match /^http.*tomato\.png/ + end + + describe "taxon" do + it "assigns the taxon matching the DFC product type" do + product = importer.import_product(supplied_product, supplier) + + expect(product.variants.first.primary_taxon).to eq(taxon) + end + end + end + + describe ".import_variant" do + let(:imported_variant) { importer.import_variant(supplied_product, supplier) } + let(:supplied_product) do + DfcProvider::SuppliedProduct.new( + "https://example.net/tomato", + name: "Tomato", + description: "Awesome tomato", + quantity: DataFoodConsortium::Connector::QuantitativeValue.new( + unit: DfcLoader.connector.MEASURES.KILOGRAM, + value: 2, + ), + productType: product_type, + catalogItems: [catalog_item], + ) + end + let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE } + let(:catalog_item) { + DataFoodConsortium::Connector::CatalogItem.new( + nil, + # On-demand is expressed as negative stock. + # And some APIs send strings instead of numbers... + stockLimitation: "-1", + offers: [offer], + ) + } + let(:offer) { + DataFoodConsortium::Connector::Offer.new( + nil, + price: DataFoodConsortium::Connector::Price.new(value: "15.50"), + ) + } + + it "creates a new Spree::Product and variant" do + create(:taxon) + + expect(imported_variant).to be_a(Spree::Variant) + expect(imported_variant).to be_valid + expect(imported_variant.id).to be_nil + expect(imported_variant.semantic_links.size).to eq 1 + + link = imported_variant.semantic_links[0] + expect(link.semantic_id).to eq "https://example.net/tomato" + + imported_product = imported_variant.product + expect(imported_product).to be_a(Spree::Product) + expect(imported_product).to be_valid + expect(imported_product.id).to be_nil + expect(imported_product.name).to eq("Tomato") + expect(imported_product.description).to eq("Awesome tomato") + expect(imported_product.variant_unit).to eq("weight") + + # Stock can only be checked when persisted: + imported_product.save! + expect(imported_variant.price).to eq 15.50 + expect(imported_variant.on_demand).to eq true + expect(imported_variant.on_hand).to eq 0 + end + + context "linked to product group" do + let(:imported_variant) { importer.import_variant(supplied_product, supplier) } + + let(:supplied_product) do + DfcProvider::SuppliedProduct.new( + "https://example.net/tomato", + name: "Tomato", + description: "Better Awesome tomato", + quantity: DataFoodConsortium::Connector::QuantitativeValue.new( + unit: DfcLoader.connector.MEASURES.KILOGRAM, + value: 2, + ), + productType: product_type, + spree_product_id: variant.product.id + ) + end + let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.DRINK.SOFT_DRINK.FRUIT_JUICE } + let!(:new_taxon) { + create( + :taxon, + name: "Fruit Juice", + dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#fruit-juice" + ) + } + + it "update an existing Spree::Product" do + imported_product = imported_variant.product + expect(imported_product.id).to eq(spree_product.id) + expect(imported_product.description).to eq("Better Awesome tomato") + expect(imported_variant.primary_taxon).to eq(new_taxon) + end + + it "copies name and description from parent product" do + supplied_product.isVariantOf << DfcProvider::SuppliedProduct.new( + "some-id", name: "Our tomatoes", description: "Choose a variety." + ) + imported_product = imported_variant.product + expect(imported_product.name).to eq "Our tomatoes" + expect(imported_product.description).to eq "Choose a variety." + end + + context "when spree_product_uri doesn't match the server host" do + let(:supplied_product) do + DfcProvider::SuppliedProduct.new( + "https://example.net/tomato", + name: "Tomato", + description: "Awesome tomato", + quantity: DataFoodConsortium::Connector::QuantitativeValue.new( + unit: DfcLoader.connector.MEASURES.KILOGRAM, + value: 2, + ), + productType: product_type, + spree_product_uri: "http://another.host/api/dfc/enterprises/10/supplied_products/50", + isVariantOf: [product_group], + ) + end + let(:product_group) do + DataFoodConsortium::Connector::SuppliedProduct.new( + "http://test.host/api/dfc/product_groups/6" + ) + end + + it "creates a new Spree::Product and variant" do + expect(imported_variant).to be_a(Spree::Variant) + expect(imported_variant.id).to be_nil + + imported_product = imported_variant.product + expect(imported_product).to be_a(Spree::Product) + expect(imported_product.id).to be_nil + expect(imported_product.name).to eq("Tomato") + expect(imported_product.description).to eq("Awesome tomato") + expect(imported_product.variant_unit).to eq("weight") + expect(imported_product.semantic_link.semantic_id) + .to eq "http://test.host/api/dfc/product_groups/6" + end + end + end + end + + describe ".referenced_spree_product" do + let(:result) { importer.referenced_spree_product(supplied_product, supplier) } + let(:supplied_product) do + DfcProvider::SuppliedProduct.new( + "https://example.net/tomato", + name: "Tomato", + ) + end + + it "returns nil when no reference is given" do + expect(result).to eq nil + end + + it "returns a product referenced by semantic id" do + variant.save! + supplied_product.isVariantOf << + DataFoodConsortium::Connector::SuppliedProduct.new( + "http://test.host/api/dfc/product_groups/6" + ) + expect(result).to eq spree_product + end + + it "returns a product referenced by external URI" do + variant.save! + supplied_product.isVariantOf << + DataFoodConsortium::Connector::SuppliedProduct.new( + "http://example.net/product_group" + ) + SemanticLink.create!( + subject: spree_product, + semantic_id: "http://example.net/product_group", + ) + expect(result).to eq spree_product + end + + it "returns a product referenced by URI" do + variant.save! + supplied_product.spree_product_uri = + "http://test.host/api/dfc/enterprises/7?spree_product_id=6" + expect(result).to eq spree_product + end + + it "doesn't return a product of another enterprise" do + variant.save! + create(:product, id: 8, supplier_id: create(:enterprise).id) + + supplied_product.spree_product_uri = + "http://test.host/api/dfc/enterprises/7?spree_product_id=8" + expect(result).to eq nil + end + + it "doesn't return a foreign product referenced by URI" do + variant.save! + supplied_product.spree_product_uri = + "http://another.host/api/dfc/enterprises/7?spree_product_id=6" + expect(result).to eq nil + end + + it "returns a product referenced by id" do + variant.save! + supplied_product.spree_product_id = "6" + expect(result).to eq spree_product + end + end +end diff --git a/spec/base_spec_helper.rb b/spec/base_spec_helper.rb index f8b326003a..f8c30b3f07 100644 --- a/spec/base_spec_helper.rb +++ b/spec/base_spec_helper.rb @@ -177,29 +177,6 @@ RSpec.configure do |config| ActionController::Base.perform_caching = caching end - # Take example responses from Rswag specs for API documentation. - # https://github.com/rswag/rswag#enable-auto-generation-examples-from-responses - config.after(:each, :rswag_autodoc) do |example| - # Categories and group operations of the same API endpoint. - example.metadata[:operation][:tags] ||= [self.class.top_level_description] - - next if response&.body.blank? - - # Include response as example in the documentation. - example.metadata[:response][:content] ||= {} - example.metadata[:response][:content].deep_merge!( - { - "application/json" => { - examples: { - test_example: { - value: JSON.parse(response.body, symbolize_names: true) - } - } - } - } - ) - end - # Show javascript errors in test output with `js_debug: true` config.after(:each, :js_debug) do errors = page.driver.browser.manage.logs.get(:browser) diff --git a/spec/requests/api/v1/customers_spec.rb b/spec/requests/api/v1/customers_spec.rb index 1259e99e03..9af5ad71d1 100644 --- a/spec/requests/api/v1/customers_spec.rb +++ b/spec/requests/api/v1/customers_spec.rb @@ -33,8 +33,8 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap produces "application/json" response "200", "Customers list" do - param(:enterprise_id) { enterprise1.id } - param("extra_fields[customer]") { :balance } + let(:enterprise_id) { enterprise1.id } + let("extra_fields[customer]") { :balance } schema '$ref': "#/components/schemas/customers_collection" run_test! @@ -176,7 +176,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap } response "201", "Minimal customer created" do - param(:customer) do + let(:customer) do { email: "test@example.com", enterprise_id: enterprise1.id.to_s @@ -196,7 +196,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap end response "201", "Example customer created" do - param(:customer) do + let(:customer) do CustomerSchema.writable_attributes.transform_values do |attribute| attribute[:example] end.merge( @@ -219,7 +219,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap end response "422", "Unpermitted parameter" do - param(:customer) do + let(:customer) do { email: "test@example.com", enterprise_id: enterprise1.id.to_s, @@ -234,7 +234,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap end response "422", "Unprocessable entity" do - param(:customer) { {} } + let(:customer) { {} } schema '$ref': "#/components/schemas/error_response" run_test! do @@ -252,7 +252,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap produces "application/json" response "200", "Customer" do - param(:id) { customer1.id } + let(:id) { customer1.id } schema CustomerSchema.schema( require_all: true, extra_fields: { name: :balance, required: true } @@ -269,7 +269,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap end response "404", "Not found" do - param(:id) { 0 } + let(:id) { 0 } schema '$ref': "#/components/schemas/error_response" run_test! do @@ -281,7 +281,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap before { logout } response "401", "Unauthorized" do - param(:id) { customer1.id } + let(:id) { customer1.id } schema '$ref': "#/components/schemas/error_response" run_test! do @@ -350,8 +350,8 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap } response "200", "Customer updated" do - param(:id) { customer1.id } - param(:customer) do + let(:id) { customer1.id } + let(:customer) do { email: "test@example.com", enterprise_id: enterprise1.id.to_s @@ -421,8 +421,8 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap end response "422", "Unprocessable entity" do - param(:id) { customer1.id } - param(:customer) { {} } + let(:id) { customer1.id } + let(:customer) { {} } schema '$ref': "#/components/schemas/error_response" run_test! @@ -435,7 +435,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap produces "application/json" response "200", "Customer deleted" do - param(:id) { customer1.id } + let(:id) { customer1.id } schema '$ref': "#/components/schemas/customer" run_test! @@ -450,7 +450,7 @@ RSpec.describe "Customers", type: :request, swagger_doc: "v1.yaml", feature: :ap produces "application/json" response "200", "Customers list" do - param(:enterprise_id) { enterprise1.id } + let(:enterprise_id) { enterprise1.id } schema '$ref': "#/components/schemas/customers_collection" run_test! diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index b3a3676cbc..266cf47311 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -71,11 +71,27 @@ RSpec.configure do |config| # the key, this may want to be changed to avoid putting yaml in json files. # Defaults to json. Accepts ':json' and ':yaml'. config.openapi_format = :yaml -end -module RswagExtension - def param(args, &) - let(args) { instance_eval(&) } + # Take example responses from Rswag specs for API documentation. + # https://github.com/rswag/rswag#enable-auto-generation-examples-from-responses + config.after(:each, swagger_doc: "dfc.yaml") do |example| + # Categories and group operations of the same API endpoint. + example.metadata[:operation][:tags] ||= [self.class.top_level_description] + + next if response&.body.blank? + + # Include response as example in the documentation. + example.metadata[:response][:content] ||= {} + example.metadata[:response][:content].deep_merge!( + { + "application/json" => { + examples: { + test_example: { + value: JSON.parse(response.body, symbolize_names: true) + } + } + } + } + ) end end -Rswag::Specs::ExampleGroupHelpers.prepend RswagExtension diff --git a/swagger/dfc.yaml b/swagger/dfc.yaml index 0724baad61..81b21a3bd3 100644 --- a/swagger/dfc.yaml +++ b/swagger/dfc.yaml @@ -177,8 +177,13 @@ paths: "@type": dfc-b:QuantitativeValue dfc-b:hasUnit: dfc-m:Gram dfc-b:value: 1.0 + dfc-b:isVariantOf: http://test.host/api/dfc/product_groups/90000 ofn:spree_product_id: 90000 ofn:spree_product_uri: http://test.host/api/dfc/enterprises/10000?spree_product_id=90000 + - "@id": http://test.host/api/dfc/product_groups/90000 + "@type": dfc-b:SuppliedProduct + dfc-b:name: Apple + dfc-b:hasVariant: http://test.host/api/dfc/enterprises/10000/supplied_products/10001 - "@id": http://test.host/api/dfc/enterprises/10000/offers/10001 "@type": dfc-b:Offer dfc-b:hasPrice: @@ -463,6 +468,7 @@ paths: dfc-b:hasUnit: dfc-m:Gram dfc-b:value: 1.0 dfc-b:image: http://test.host/rails/active_storage/url/logo-white.png + dfc-b:isVariantOf: http://test.host/api/dfc/product_groups/90000 ofn:spree_product_id: 90000 ofn:spree_product_uri: http://test.host/api/dfc/enterprises/10000?spree_product_id=90000 ofn:image: http://test.host/rails/active_storage/url/logo-white.png @@ -552,6 +558,35 @@ paths: "@type": dfc-b:Person '404': description: not found + "/api/dfc/product_groups/{id}": + parameters: + - name: enterprise_id + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + get: + summary: Show ProductGroup + tags: + - ProductGroups + responses: + '200': + description: success + content: + application/json: + examples: + test_example: + value: + "@context": https://www.datafoodconsortium.org + "@id": http://test.host/api/dfc/product_groups/90000 + "@type": dfc-b:SuppliedProduct + dfc-b:name: Pesto + dfc-b:hasVariant: http://test.host/api/dfc/enterprises/10000/supplied_products/10001 "/api/dfc/enterprises/{enterprise_id}/social_medias/{name}": get: summary: Show social media @@ -615,6 +650,7 @@ paths: "@type": dfc-b:QuantitativeValue dfc-b:hasUnit: dfc-m:Gram dfc-b:value: 6.0 + dfc-b:isVariantOf: http://test.host/api/dfc/product_groups/90000 ofn:spree_product_id: 90000 ofn:spree_product_uri: http://test.host/api/dfc/enterprises/10000?spree_product_id=90000 dfc-b:image: http://test.host/rails/active_storage/url/logo-white.png @@ -675,6 +711,7 @@ paths: dfc-b:hasUnit: dfc-m:Gram dfc-b:value: 1.0 dfc-b:image: http://test.host/rails/active_storage/url/logo-white.png + dfc-b:isVariantOf: http://test.host/api/dfc/product_groups/90000 ofn:spree_product_id: 90000 ofn:spree_product_uri: http://test.host/api/dfc/enterprises/10000?spree_product_id=90000 ofn:image: http://test.host/rails/active_storage/url/logo-white.png