Merge pull request #11378 from mkllnk/dfc-ofn-product-group-id

[DFC API] Add custom OFN product id to DFC SuppliedProduct
This commit is contained in:
Gaetan Craig-Riou
2023-09-22 17:57:11 +10:00
committed by GitHub
17 changed files with 144 additions and 56 deletions

View File

@@ -39,6 +39,7 @@ Metrics/BlockLength:
"put",
"resource",
"resources",
"response",
"scenario",
"shared_examples",
"shared_examples_for",

View File

@@ -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?

View File

@@ -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

View File

@@ -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.

View File

@@ -1,7 +1,5 @@
# frozen_string_literal: true
require "data_food_consortium/connector/connector"
class DfcLoader
def self.connector
@connector ||= load_vocabularies

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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) }

View File

@@ -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],
)
}

View File

@@ -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],
)
}

View File

@@ -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

View File

@@ -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) }

View File

@@ -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: