diff --git a/.rubocop_styleguide.yml b/.rubocop_styleguide.yml index a6a2fab4f4..5459d63ebc 100644 --- a/.rubocop_styleguide.yml +++ b/.rubocop_styleguide.yml @@ -39,6 +39,7 @@ Metrics/BlockLength: "put", "resource", "resources", + "response", "scenario", "shared_examples", "shared_examples_for", diff --git a/app/models/spree/product.rb b/app/models/spree/product.rb index e7bf67a3d3..be905c8ccc 100755 --- a/app/models/spree/product.rb +++ b/app/models/spree/product.rb @@ -272,18 +272,6 @@ module Spree end end - private - - def update_units - return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name? - - variants.each(&:update_units) - end - - def touch_distributors - Enterprise.distributing_products(id).each(&:touch) - end - def ensure_standard_variant return unless variants.empty? @@ -298,6 +286,18 @@ module Spree variants << variant end + private + + def update_units + return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name? + + variants.each(&:update_units) + end + + def touch_distributors + Enterprise.distributing_products(id).each(&:touch) + end + def validate_image return if image.blank? || image.valid? 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 e4daf3155f..21b4c4b59d 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,13 +14,19 @@ module DfcProvider return head :bad_request unless supplied_product - product = SuppliedProductBuilder.import(supplied_product) - product.supplier = current_enterprise - product.save! + variant = SuppliedProductBuilder.import_variant(supplied_product) + product = variant.product - supplied_product = SuppliedProductBuilder.supplied_product( - product.variants.first - ) + if product.new_record? + product.supplier = current_enterprise + product.save! + end + + if variant.new_record? + variant.save! + end + + supplied_product = SuppliedProductBuilder.supplied_product(variant) render json: DfcIo.export(supplied_product) end diff --git a/engines/dfc_provider/app/services/dfc_io.rb b/engines/dfc_provider/app/services/dfc_io.rb index 2457eb4a46..99bf63df66 100644 --- a/engines/dfc_provider/app/services/dfc_io.rb +++ b/engines/dfc_provider/app/services/dfc_io.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -# Load our monkey-patches: -require "data_food_consortium/connector/connector" - # Our interface to the DFC Connector library. module DfcIo # Serialise DFC Connector subjects as JSON-LD string. diff --git a/engines/dfc_provider/app/services/dfc_loader.rb b/engines/dfc_provider/app/services/dfc_loader.rb index d9ef128c96..1fae62f9f4 100644 --- a/engines/dfc_provider/app/services/dfc_loader.rb +++ b/engines/dfc_provider/app/services/dfc_loader.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "data_food_consortium/connector/connector" - class DfcLoader def self.connector @connector ||= load_vocabularies diff --git a/engines/dfc_provider/app/services/supplied_product_builder.rb b/engines/dfc_provider/app/services/supplied_product_builder.rb index fe036dcfb1..a706a72eb7 100644 --- a/engines/dfc_provider/app/services/supplied_product_builder.rb +++ b/engines/dfc_provider/app/services/supplied_product_builder.rb @@ -7,16 +7,35 @@ class SuppliedProductBuilder < DfcBuilder id: variant.id, ) - DataFoodConsortium::Connector::SuppliedProduct.new( + DfcProvider::SuppliedProduct.new( id, name: variant.name_to_display, description: variant.description, productType: product_type, quantity: QuantitativeValueBuilder.quantity(variant), + spree_product_id: variant.product.id, ) end - def self.import(supplied_product) + def self.import_variant(supplied_product) + product_id = supplied_product.spree_product_id + + if product_id.present? + product = Spree::Product.find(product_id) + Spree::Variant.new( + product:, + price: 0, + ).tap do |variant| + apply(supplied_product, variant) + end + else + product = import_product(supplied_product) + product.ensure_standard_variant + product.variants.first + end + end + + def self.import_product(supplied_product) Spree::Product.new( name: supplied_product.name, description: supplied_product.description, diff --git a/engines/dfc_provider/lib/data_food_consortium/connector/importer.rb b/engines/dfc_provider/lib/data_food_consortium/connector/importer.rb index e46a021d73..b8365cdc8a 100644 --- a/engines/dfc_provider/lib/data_food_consortium/connector/importer.rb +++ b/engines/dfc_provider/lib/data_food_consortium/connector/importer.rb @@ -15,21 +15,28 @@ module DataFoodConsortium ].freeze def self.type_map - @type_map ||= TYPES.each_with_object({}) do |clazz, result| - # Methods with variable arguments have a negative arity of -n-1 - # where n is the number of required arguments. - number_of_required_args = -1 * (clazz.instance_method(:initialize).arity + 1) - args = Array.new(number_of_required_args) - type_uri = clazz.new(*args).semanticType - result[type_uri] = clazz - - # Add support for the old DFC v1.7 URLs: - new_type_uri = type_uri.gsub( - "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#", - "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#" - ) - result[new_type_uri] = clazz + unless @type_map + @type_map = {} + TYPES.each(&method(:register_type)) end + + @type_map + end + + def self.register_type(clazz) + # Methods with variable arguments have a negative arity of -n-1 + # where n is the number of required arguments. + number_of_required_args = -1 * (clazz.instance_method(:initialize).arity + 1) + args = Array.new(number_of_required_args) + type_uri = clazz.new(*args).semanticType + type_map[type_uri] = clazz + + # Add support for the old DFC v1.7 URLs: + new_type_uri = type_uri.gsub( + "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#", + "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#" + ) + type_map[new_type_uri] = clazz end def import(json_string_or_io) @@ -120,11 +127,12 @@ module DataFoodConsortium end def guess_setter_name(predicate) - fragment = predicate.fragment - - # Some predicates are named like `hasQuantity` - # but the attribute name would be `quantity`. - name = fragment.sub(/^has/, "").camelize(:lower) + name = + # Some predicates are named like `hasQuantity` + # but the attribute name would be `quantity`. + predicate.fragment&.sub(/^has/, "")&.camelize(:lower) || + # And sometimes the URI looks like `ofn:spree_product_id`. + predicate.to_s.split(":").last "#{name}=" end diff --git a/engines/dfc_provider/lib/dfc_provider.rb b/engines/dfc_provider/lib/dfc_provider.rb index b0cf98121a..4676736439 100644 --- a/engines/dfc_provider/lib/dfc_provider.rb +++ b/engines/dfc_provider/lib/dfc_provider.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true +# Load our monkey-patches of the DFC Connector: +require "data_food_consortium/connector/connector" + +# Our Rails engine require "dfc_provider/engine" +# Custom data types +require "dfc_provider/supplied_product" + module DfcProvider + DataFoodConsortium::Connector::Importer.register_type(SuppliedProduct) end diff --git a/engines/dfc_provider/lib/dfc_provider/supplied_product.rb b/engines/dfc_provider/lib/dfc_provider/supplied_product.rb new file mode 100644 index 0000000000..06b762ba87 --- /dev/null +++ b/engines/dfc_provider/lib/dfc_provider/supplied_product.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module DfcProvider + class SuppliedProduct < DataFoodConsortium::Connector::SuppliedProduct + attr_accessor :spree_product_id + + def initialize(semantic_id, spree_product_id: nil, **properties) + super(semantic_id, **properties) + + self.spree_product_id = spree_product_id + + registerSemanticProperty("ofn:spree_product_id") do + self.spree_product_id + end + end + end +end diff --git a/engines/dfc_provider/spec/lib/data_food_consortium/connector/connector_spec.rb b/engines/dfc_provider/spec/lib/data_food_consortium/connector/connector_spec.rb index 48d7cbd3e6..f0bda34bf9 100644 --- a/engines/dfc_provider/spec/lib/data_food_consortium/connector/connector_spec.rb +++ b/engines/dfc_provider/spec/lib/data_food_consortium/connector/connector_spec.rb @@ -21,7 +21,7 @@ describe DataFoodConsortium::Connector::Connector, vcr: true do it "imports" do json = connector.export(product) result = connector.import(json) - expect(result.class).to eq product.class + expect(result).to be_a product.class expect(result.semanticType).to eq product.semanticType expect(result.semanticId).to eq "https://example.net/tomato" expect(result.name).to eq "Tomato" @@ -32,7 +32,7 @@ describe DataFoodConsortium::Connector::Connector, vcr: true do io = StringIO.new(json) result = connector.import(io) - expect(result.class).to eq product.class + expect(result).to be_a product.class expect(result.semanticType).to eq product.semanticType expect(result.semanticId).to eq "https://example.net/tomato" expect(result.name).to eq "Tomato" diff --git a/engines/dfc_provider/spec/lib/data_food_consortium/connector/importer_spec.rb b/engines/dfc_provider/spec/lib/data_food_consortium/connector/importer_spec.rb index e64dce16a7..04e9407ce6 100644 --- a/engines/dfc_provider/spec/lib/data_food_consortium/connector/importer_spec.rb +++ b/engines/dfc_provider/spec/lib/data_food_consortium/connector/importer_spec.rb @@ -100,7 +100,7 @@ describe DataFoodConsortium::Connector::Importer do it "imports a single object with simple properties" do result = import(product) - expect(result.class).to eq product.class + expect(result).to be_a product.class expect(result.semanticType).to eq product.semanticType expect(result.semanticId).to eq "https://example.net/tomato" expect(result.name).to eq "Tomato" @@ -111,7 +111,7 @@ describe DataFoodConsortium::Connector::Importer do it "imports an object with referenced context" do result = connector.import(product_data) - expect(result.class).to eq DataFoodConsortium::Connector::SuppliedProduct + expect(result).to be_a DataFoodConsortium::Connector::SuppliedProduct expect(result.semanticType).to eq "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#SuppliedProduct" expect(result.semanticId).to eq "https://example.net/tomato" expect(result.name).to eq "Tomato" @@ -122,7 +122,7 @@ describe DataFoodConsortium::Connector::Importer do it "imports an object with included context" do result = connector.import(product_data_with_context) - expect(result.class).to eq DataFoodConsortium::Connector::SuppliedProduct + expect(result).to be_a DataFoodConsortium::Connector::SuppliedProduct expect(result.semanticType).to eq "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#SuppliedProduct" expect(result.semanticId).to eq "https://example.net/tomato" expect(result.name).to eq "Tomato" @@ -133,7 +133,7 @@ describe DataFoodConsortium::Connector::Importer do it "imports an object with DFC v1.8 context" do result = connector.import(product_data_with_context_v1_8) - expect(result.class).to eq DataFoodConsortium::Connector::SuppliedProduct + expect(result).to be_a DataFoodConsortium::Connector::SuppliedProduct expect(result.semanticType).to eq "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#SuppliedProduct" expect(result.semanticId).to eq "https://example.net/tomato" expect(result.name).to eq "Tomato" @@ -149,7 +149,7 @@ describe DataFoodConsortium::Connector::Importer do item, tomato = result - expect(item.class).to eq catalog_item.class + expect(item).to be_a catalog_item.class expect(item.semanticType).to eq catalog_item.semanticType expect(item.semanticId).to eq "https://example.net/tomatoItem" expect(tomato.name).to eq "Tomato" diff --git a/engines/dfc_provider/spec/requests/addresses_spec.rb b/engines/dfc_provider/spec/requests/addresses_spec.rb index 9848c9244b..21032e5df9 100644 --- a/engines/dfc_provider/spec/requests/addresses_spec.rb +++ b/engines/dfc_provider/spec/requests/addresses_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require DfcProvider::Engine.root.join("spec/swagger_helper") +require_relative "../swagger_helper" describe "Addresses", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do let(:user) { create(:oidc_user) } diff --git a/engines/dfc_provider/spec/requests/catalog_items_spec.rb b/engines/dfc_provider/spec/requests/catalog_items_spec.rb index 0d401f9090..c082630044 100644 --- a/engines/dfc_provider/spec/requests/catalog_items_spec.rb +++ b/engines/dfc_provider/spec/requests/catalog_items_spec.rb @@ -15,7 +15,7 @@ describe "CatalogItems", type: :request, swagger_doc: "dfc.yaml", let(:product) { create( :base_product, - supplier: enterprise, name: "Apple", description: "Red", + id: 90_000, supplier: enterprise, name: "Apple", description: "Red", variants: [variant], ) } diff --git a/engines/dfc_provider/spec/requests/enterprises_spec.rb b/engines/dfc_provider/spec/requests/enterprises_spec.rb index 3c3835452f..0e5f885490 100644 --- a/engines/dfc_provider/spec/requests/enterprises_spec.rb +++ b/engines/dfc_provider/spec/requests/enterprises_spec.rb @@ -22,7 +22,7 @@ describe "Enterprises", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: let!(:product) { create( :base_product, - supplier: enterprise, name: "Apple", description: "Round", + id: 90_000, supplier: enterprise, name: "Apple", description: "Round", variants: [variant], ) } diff --git a/engines/dfc_provider/spec/requests/supplied_products_spec.rb b/engines/dfc_provider/spec/requests/supplied_products_spec.rb index b310fff021..a3e2ed2d77 100644 --- a/engines/dfc_provider/spec/requests/supplied_products_spec.rb +++ b/engines/dfc_provider/spec/requests/supplied_products_spec.rb @@ -9,6 +9,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", let!(:product) { create( :base_product, + id: 90_000, supplier: enterprise, name: "Pesto", description: "Basil Pesto", variants: [variant], ) @@ -78,7 +79,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", example.metadata[:operation][:parameters].first[:schema][:example] end - it "creates a variant" do |example| + it "creates a product and variant" do |example| expect { submit_request(example.metadata) } .to change { enterprise.supplied_products.count }.by(1) @@ -87,16 +88,44 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", %r|^http://test\.host/api/dfc/enterprises/10000/supplied_products/[0-9]+$| ) + spree_product_id = json_response["ofn:spree_product_id"].to_i + variant_id = dfc_id.split("/").last.to_i variant = Spree::Variant.find(variant_id) expect(variant.name).to eq "Apple" expect(variant.unit_value).to eq 3 + expect(variant.product_id).to eq spree_product_id + + # References the associated Spree::Product + product_id = json_response["ofn:spree_product_id"] + product = Spree::Product.find(product_id) + expect(product.name).to eq "Apple" + expect(product.variants).to eq [variant] + + # Creates a variant for existing product + supplied_product[:'ofn:spree_product_id'] = product_id + supplied_product[:'dfc-b:hasQuantity'][:'dfc-b:value'] = 6 + + expect { + submit_request(example.metadata) + product.variants.reload + } + .to change { product.variants.count }.by(1) + + variant_id = json_response["@id"].split("/").last.to_i + second_variant = Spree::Variant.find(variant_id) + expect(product.variants).to match_array [variant, second_variant] + expect(second_variant.unit_value).to eq 6 # Insert static value to keep documentation deterministic: response.body.gsub!( "supplied_products/#{variant_id}", "supplied_products/10001" ) + .gsub!( + "\"ofn:spree_product_id\":#{spree_product_id}", + '"ofn:spree_product_id":90000' + ) end end end @@ -116,6 +145,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", run_test! do expect(response.body).to include variant.name + expect(json_response["ofn:spree_product_id"]).to eq 90_000 end end diff --git a/engines/dfc_provider/spec/services/address_builder_spec.rb b/engines/dfc_provider/spec/services/address_builder_spec.rb index 72fc0f08c0..660c7781ae 100644 --- a/engines/dfc_provider/spec/services/address_builder_spec.rb +++ b/engines/dfc_provider/spec/services/address_builder_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require DfcProvider::Engine.root.join("spec/spec_helper") +require_relative "../spec_helper" describe AddressBuilder do subject(:result) { described_class.address(address) } diff --git a/swagger/dfc.yaml b/swagger/dfc.yaml index 8927ccfc1d..f08222b960 100644 --- a/swagger/dfc.yaml +++ b/swagger/dfc.yaml @@ -118,6 +118,7 @@ paths: dfc-b:lifetime: '' dfc-b:usageOrStorageCondition: '' dfc-b:totalTheoreticalStock: 0.0 + ofn:spree_product_id: 90000 - "@id": http://test.host/api/dfc/enterprises/10000/offers/10001 "@type": dfc-b:Offer dfc-b:price: 19.99 @@ -341,6 +342,7 @@ paths: dfc-b:lifetime: '' dfc-b:usageOrStorageCondition: '' dfc-b:totalTheoreticalStock: 0.0 + ofn:spree_product_id: 90000 - "@id": http://test.host/api/dfc/enterprises/10000/catalog_items/10001 "@type": dfc-b:CatalogItem dfc-b:references: http://test.host/api/dfc/enterprises/10000/supplied_products/10001 @@ -409,6 +411,7 @@ paths: dfc-b:lifetime: '' dfc-b:usageOrStorageCondition: '' dfc-b:totalTheoreticalStock: 0.0 + ofn:spree_product_id: 90000 requestBody: content: application/json: @@ -471,6 +474,7 @@ paths: dfc-b:lifetime: '' dfc-b:usageOrStorageCondition: '' dfc-b:totalTheoreticalStock: 0.0 + ofn:spree_product_id: 90000 '404': description: not found put: