diff --git a/app/services/weights_and_measures.rb b/app/services/weights_and_measures.rb index c2bc8c313c..ada5523e22 100644 --- a/app/services/weights_and_measures.rb +++ b/app/services/weights_and_measures.rb @@ -24,16 +24,24 @@ class WeightsAndMeasures UNITS = { 'weight' => { + 0.001 => { 'name' => 'mg', 'system' => 'metric' }, 1.0 => { 'name' => 'g', 'system' => 'metric' }, - 28.35 => { 'name' => 'oz', 'system' => 'imperial' }, - 453.6 => { 'name' => 'lb', 'system' => 'imperial' }, 1000.0 => { 'name' => 'kg', 'system' => 'metric' }, - 1_000_000.0 => { 'name' => 'T', 'system' => 'metric' } + 1_000_000.0 => { 'name' => 'T', 'system' => 'metric' }, + + 28.349523125 => { 'name' => 'oz', 'system' => 'imperial' }, + 28.35 => { 'name' => 'oz', 'system' => 'imperial' }, + 453.59237 => { 'name' => 'lb', 'system' => 'imperial' }, + 453.6 => { 'name' => 'lb', 'system' => 'imperial' }, }, 'volume' => { 0.001 => { 'name' => 'mL', 'system' => 'metric' }, + 0.01 => { 'name' => 'cL', 'system' => 'metric' }, + 0.1 => { 'name' => 'dL', 'system' => 'metric' }, 1.0 => { 'name' => 'L', 'system' => 'metric' }, - 1000.0 => { 'name' => 'kL', 'system' => 'metric' } + 1000.0 => { 'name' => 'kL', 'system' => 'metric' }, + + 4.54609 => { 'name' => 'gal', 'system' => 'imperial' }, } }.freeze 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 963e040be3..4edcbf19c3 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 @@ -30,17 +30,14 @@ module DfcProvider end def update - dfc_request = JSON.parse(request.body.read) - return unless dfc_request.key?("dfc-b:description") + supplied_product = import&.first - variant.product.update!( - description: dfc_request["dfc-b:description"], - ) + return head :bad_request unless supplied_product - # This input is DFC v1.6 currently sent by the DFC Prototype. - variant.update!( - unit_value: dfc_request["dfc-b:quantity"], - ) + SuppliedProductBuilder.apply(supplied_product, variant) + + variant.product.save! + variant.save! end private diff --git a/engines/dfc_provider/app/services/quantitative_value_builder.rb b/engines/dfc_provider/app/services/quantitative_value_builder.rb index 8291b3c0b0..b3328713e3 100644 --- a/engines/dfc_provider/app/services/quantitative_value_builder.rb +++ b/engines/dfc_provider/app/services/quantitative_value_builder.rb @@ -28,17 +28,90 @@ class QuantitativeValueBuilder < DfcBuilder end def self.apply(quantity, product) - product.variant_unit, product.variant_unit_name = - case quantity.unit - when DfcLoader.connector.MEASURES.UNIT.QUANTITYUNIT.LITRE - ["volume", "liter"] - when DfcLoader.connector.MEASURES.UNIT.QUANTITYUNIT.GRAM - ["weight", "gram"] - else - ["items", "items"] - end + measure, unit_name, unit_scale = map_unit(quantity.unit) - product.variant_unit_scale = 1 - product.unit_value = quantity.value + product.variant_unit = measure + product.variant_unit_name = unit_name if measure == "items" + product.variant_unit_scale = unit_scale + product.unit_value = quantity.value * unit_scale + end + + # Map DFC units to OFN fields: + # + # - variant_unit + # - variant_unit_name + # - variant_unit_scale + # + # Unimplemented measures + # + # The DFC knows lots of single piece measures like a tub. There are not + # listed here and automatically mapped to "item". The following is a list + # of measures we want or could implement. + # + # Length is not represented in the OFN: + # + # :CENTIMETRE, + # :DECIMETRE, + # :METRE, + # :KILOMETRE, + # :INCH, + # + # Other: + # + # :PERCENT, + # + # This method is quite long and may be shortened with new DFC features: + # + # * https://github.com/datafoodconsortium/taxonomies/issues/7 + # * https://github.com/datafoodconsortium/connector-ruby/issues/18 + # + # Until then, we can ignore Rubocop metrics, IMO. + def self.map_unit(unit) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength + quantity_unit = DfcLoader.connector.MEASURES.UNIT.QUANTITYUNIT + + # The unit name is only set for items. The name is implied for weight and + # volume and filled in by `WeightsAndMeasures`. + case unit + when quantity_unit.LITRE + ["volume", nil, 1] + when quantity_unit.MILLILITRE + ["volume", nil, 0.001] + when quantity_unit.CENTILITRE + ["volume", nil, 0.01] + when quantity_unit.DECILITRE + ["volume", nil, 0.1] + when quantity_unit.GALLON + ["volume", nil, 4.54609] + + when quantity_unit.MILLIGRAM + ["weight", nil, 0.001] + when quantity_unit.GRAM + ["weight", nil, 1] + when quantity_unit.KILOGRAM + ["weight", nil, 1_000] + when quantity_unit.TONNE + ["weight", nil, 1_000_000] + # Not part of the DFC yet: + # when quantity_unit.OUNCE + # ["weight", nil, 28.349523125] + when quantity_unit.POUNDMASS + ["weight", nil, 453.59237] + + when quantity_unit.PAIR + ["items", "pair", 2] + when quantity_unit._4PACK + ["items", "4 pack", 4] + when quantity_unit._6PACK + ["items", "6 pack", 6] + when quantity_unit.HALFDOZEN + ["items", "half dozen", 6] + when quantity_unit.DOZEN + ["items", "dozen", 12] + else + # Labels may be provided one day: + # https://github.com/datafoodconsortium/connector-ruby/issues/18 + label = unit.try(:semanticId)&.split("#")&.last || "items" + ["items", label, 1] + end 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 02c8f3e00a..fe036dcfb1 100644 --- a/engines/dfc_provider/app/services/supplied_product_builder.rb +++ b/engines/dfc_provider/app/services/supplied_product_builder.rb @@ -26,4 +26,14 @@ class SuppliedProductBuilder < DfcBuilder QuantitativeValueBuilder.apply(supplied_product.quantity, product) end end + + def self.apply(supplied_product, variant) + variant.product.assign_attributes( + name: supplied_product.name, + description: supplied_product.description, + ) + + QuantitativeValueBuilder.apply(supplied_product.quantity, variant.product) + variant.unit_value = variant.product.unit_value + end end diff --git a/engines/dfc_provider/spec/fixtures/files/patch_supplied_product.json b/engines/dfc_provider/spec/fixtures/files/patch_supplied_product.json deleted file mode 100644 index 86230c7e7a..0000000000 --- a/engines/dfc_provider/spec/fixtures/files/patch_supplied_product.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "@context": { - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "skos": "http://www.w3.org/2004/02/skos/core#", - "dfc": "http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl#", - "dc": "http://purl.org/dc/elements/1.1/#", - "dfc-b": "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", - "dfc-p": "http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl#", - "dfc-t": "http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl#", - "dfc-m": "http://static.datafoodconsortium.org/data/measures.rdf#", - "dfc-pt": "http://static.datafoodconsortium.org/data/productTypes.rdf#", - "dfc-f": "http://static.datafoodconsortium.org/data/facets.rdf#", - "dfc-p:hasUnit": { - "@type": "@id" - }, - "dfc-b:hasUnit": { - "@type": "@id" - }, - "dfc-b:hasQuantity": { - "@type": "@id" - }, - "dfc-p:hasType": { - "@type": "@id" - }, - "dfc-b:hasType": { - "@type": "@id" - }, - "dfc-b:references": { - "@type": "@id" - }, - "dfc-b:referencedBy": { - "@type": "@id" - }, - "dfc-b:offeres": { - "@type": "@id" - }, - "dfc-b:supplies": { - "@type": "@id" - }, - "dfc-b:defines": { - "@type": "@id" - }, - "dfc-b:affiliates": { - "@type": "@id" - }, - "dfc-b:manages": { - "@type": "@id" - }, - "dfc-b:offeredThrough": { - "@type": "@id" - }, - "dfc-b:hasBrand": { - "@type": "@id" - }, - "dfc-b:hasGeographicalOrigin": { - "@type": "@id" - }, - "dfc-b:hasClaim": { - "@type": "@id" - }, - "dfc-b:hasAllergenDimension": { - "@type": "@id" - }, - "dfc-b:hasNutrimentDimension": { - "@type": "@id" - }, - "dfc-b:hasPhysicalDimension": { - "@type": "@id" - }, - "dfc:owner": { - "@type": "@id" - }, - "dfc-t:hostedBy": { - "@type": "@id" - }, - "dfc-t:hasPivot": { - "@type": "@id" - }, - "dfc-t:represent": { - "@type": "@id" - } - }, - "dfc-b:description": "DFC-Pesto updated", - "dfc-b:quantity": 17 -} diff --git a/engines/dfc_provider/spec/fixtures/files/put_supplied_product.json b/engines/dfc_provider/spec/fixtures/files/put_supplied_product.json new file mode 100644 index 0000000000..cc081912bb --- /dev/null +++ b/engines/dfc_provider/spec/fixtures/files/put_supplied_product.json @@ -0,0 +1,105 @@ +{ + "@context": { + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "skos": "http://www.w3.org/2004/02/skos/core#", + "dfc": "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_FullModel.owl#", + "dc": "http://purl.org/dc/elements/1.1/#", + "dfc-b": "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#", + "dfc-p": "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_ProductGlossary.owl#", + "dfc-t": "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_TechnicalOntology.owl#", + "dfc-m": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/measures.rdf#", + "dfc-pt": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#", + "dfc-f": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/facets.rdf#", + "ontosec": "http://www.semanticweb.org/ontologies/2008/11/OntologySecurity.owl#", + "dfc-p:hasUnit": { + "@type": "@id" + }, + "dfc-b:hasUnit": { + "@type": "@id" + }, + "dfc-b:hasQuantity": { + "@type": "@id" + }, + "dfc-p:hasType": { + "@type": "@id" + }, + "dfc-b:hasType": { + "@type": "@id" + }, + "dfc-b:references": { + "@type": "@id" + }, + "dfc-b:referencedBy": { + "@type": "@id" + }, + "dfc-b:offeres": { + "@type": "@id" + }, + "dfc-b:supplies": { + "@type": "@id" + }, + "dfc-b:defines": { + "@type": "@id" + }, + "dfc-b:affiliates": { + "@type": "@id" + }, + "dfc-b:hasCertification": { + "@type": "@id" + }, + "dfc-b:manages": { + "@type": "@id" + }, + "dfc-b:offeredThrough": { + "@type": "@id" + }, + "dfc-b:hasBrand": { + "@type": "@id" + }, + "dfc-b:hasGeographicalOrigin": { + "@type": "@id" + }, + "dfc-b:hasClaim": { + "@type": "@id" + }, + "dfc-b:hasAllergenDimension": { + "@type": "@id" + }, + "dfc-b:hasNutrientDimension": { + "@type": "@id" + }, + "dfc-b:hasPhysicalDimension": { + "@type": "@id" + }, + "dfc:owner": { + "@type": "@id" + }, + "dfc-t:hostedBy": { + "@type": "@id" + }, + "dfc-t:hasPivot": { + "@type": "@id" + }, + "dfc-t:represent": { + "@type": "@id" + } + }, + "@id": "https://staging.coopcircuits.fr/api/dfc-v1.7/enterprises/2731/supplied_products/56790", + "@type": "dfc-b:SuppliedProduct", + "dfc-b:alcoholPercentage": 0, + "dfc-b:description": "DFC-Pesto updated", + "dfc-b:hasQuantity": { + "@type": "dfc-b:QuantitativeValue", + "dfc-b:hasUnit": "dfc-m:Piece", + "dfc-b:value": 17 + }, + "dfc-b:hasType": "dfc-pt:non-local-vegetable", + "dfc-b:lifetime": "", + "dfc-b:name": "Pesto novo", + "dfc-b:totalTheoreticalStock": 0, + "dfc-b:usageOrStorageCondition": "", + "dfc:owner": "http://proto.datafoodconsortium.org:3000/ldp/user/64c1d30351ecb4367037a9f6", + "dfc-b:hasPhysicalCharacteristic": [], + "dfc-b:hasNutrientCharacteristic": [], + "dfc-b:hasAllergenCharacteristic": [] +} diff --git a/engines/dfc_provider/spec/requests/supplied_products_spec.rb b/engines/dfc_provider/spec/requests/supplied_products_spec.rb index 689487bddb..c4d9ef1d6e 100644 --- a/engines/dfc_provider/spec/requests/supplied_products_spec.rb +++ b/engines/dfc_provider/spec/requests/supplied_products_spec.rb @@ -32,6 +32,9 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc-v1.7/swagger.yaml 'dfc-b': "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", 'dfc-m': "http://static.datafoodconsortium.org/data/measures.rdf#", 'dfc-pt': "http://static.datafoodconsortium.org/data/productTypes.rdf#", + 'dfc-b:hasUnit': { + '@type': "@id" + }, }, '@id': "http://test.host/api/dfc-v1.7/enterprises/6201/supplied_products/0", '@type': "dfc-b:SuppliedProduct", @@ -128,7 +131,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc-v1.7/swagger.yaml consumes "application/json" parameter name: :supplied_product, in: :body, schema: { - example: ExampleJson.read("patch_supplied_product") + example: ExampleJson.read("put_supplied_product") } let(:id) { variant.id } @@ -148,6 +151,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc-v1.7/swagger.yaml submit_request(example.metadata) variant.reload }.to change { variant.description }.to("DFC-Pesto updated") + .and change { variant.name }.to("Pesto novo") .and change { variant.unit_value }.to(17) end end diff --git a/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb b/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb index b7d003a0ed..bd744fa568 100644 --- a/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb +++ b/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb @@ -43,4 +43,93 @@ describe QuantitativeValueBuilder do expect(quantity.unit.semanticId).to eq "dfc-m:Piece" end end + + describe ".apply" do + let(:quantity_unit) { DfcLoader.connector.MEASURES.UNIT.QUANTITYUNIT } + let(:product) { Spree::Product.new } + + it "uses items for anything unknown" do + quantity = DataFoodConsortium::Connector::QuantitativeValue.new( + unit: quantity_unit.JAR, + value: 3, + ) + + builder.apply(quantity, product) + + expect(product.variant_unit).to eq "items" + expect(product.variant_unit_name).to eq "Jar" + expect(product.variant_unit_scale).to eq 1 + expect(product.unit_value).to eq 3 + end + + it "knows metric units" do + quantity = DataFoodConsortium::Connector::QuantitativeValue.new( + unit: quantity_unit.LITRE, + value: 2, + ) + + builder.apply(quantity, product) + + expect(product.variant_unit).to eq "volume" + expect(product.variant_unit_name).to eq nil + expect(product.variant_unit_scale).to eq 1 + expect(product.unit_value).to eq 2 + end + + it "knows metric units with a scale in OFN" do + quantity = DataFoodConsortium::Connector::QuantitativeValue.new( + unit: quantity_unit.KILOGRAM, + value: 4, + ) + + builder.apply(quantity, product) + + expect(product.variant_unit).to eq "weight" + expect(product.variant_unit_name).to eq nil + expect(product.variant_unit_scale).to eq 1_000 + expect(product.unit_value).to eq 4_000 + end + + it "knows metric units with a small scale" do + quantity = DataFoodConsortium::Connector::QuantitativeValue.new( + unit: quantity_unit.MILLIGRAM, + value: 5, + ) + + builder.apply(quantity, product) + + expect(product.variant_unit).to eq "weight" + expect(product.variant_unit_name).to eq nil + expect(product.variant_unit_scale).to eq 0.001 + expect(product.unit_value).to eq 0.005 + end + + it "knows imperial units" do + quantity = DataFoodConsortium::Connector::QuantitativeValue.new( + unit: quantity_unit.POUNDMASS, + value: 10, + ) + + builder.apply(quantity, product) + + expect(product.variant_unit).to eq "weight" + expect(product.variant_unit_name).to eq nil + expect(product.variant_unit_scale).to eq 453.59237 + expect(product.unit_value).to eq 4_535.9237 + end + + it "knows customary units" do + quantity = DataFoodConsortium::Connector::QuantitativeValue.new( + unit: quantity_unit.DOZEN, + value: 2, + ) + + builder.apply(quantity, product) + + expect(product.variant_unit).to eq "items" + expect(product.variant_unit_name).to eq "dozen" + expect(product.variant_unit_scale).to eq 12 + expect(product.unit_value).to eq 24 + end + end end diff --git a/spec/services/variant_units/option_value_namer_spec.rb b/spec/services/variant_units/option_value_namer_spec.rb index a902d35306..cf9178c244 100644 --- a/spec/services/variant_units/option_value_namer_spec.rb +++ b/spec/services/variant_units/option_value_namer_spec.rb @@ -115,8 +115,8 @@ module VariantUnits p = double(:product, variant_unit: 'volume', variant_unit_scale: scale) allow(v).to receive(:product) { p } allow(p).to receive(:persisted?) { true } - allow(v).to receive(:unit_value) { 100 * scale } - expect(subject.send(:option_value_value_unit)).to eq [100, unit] + allow(v).to receive(:unit_value) { 3 * scale } + expect(subject.send(:option_value_value_unit)).to eq [3, unit] end end diff --git a/swagger/dfc-v1.7/swagger.yaml b/swagger/dfc-v1.7/swagger.yaml index 9ca116cf4f..431be36edb 100644 --- a/swagger/dfc-v1.7/swagger.yaml +++ b/swagger/dfc-v1.7/swagger.yaml @@ -330,7 +330,7 @@ paths: dfc-b:hasType: http://static.datafoodconsortium.org/data/productTypes.rdf#non-local-vegetable dfc-b:hasQuantity: "@type": dfc-b:QuantitativeValue - dfc-b:hasUnit: dfc-m:Piece + dfc-b:hasUnit: dfc-m:Gram dfc-b:value: 3.0 dfc-b:alcoholPercentage: 0.0 dfc-b:lifetime: '' @@ -345,6 +345,8 @@ paths: dfc-b: http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl# dfc-m: http://static.datafoodconsortium.org/data/measures.rdf# dfc-pt: http://static.datafoodconsortium.org/data/productTypes.rdf# + dfc-b:hasUnit: + "@type": "@id" "@id": http://test.host/api/dfc-v1.7/enterprises/6201/supplied_products/0 "@type": dfc-b:SuppliedProduct dfc-b:name: Apple @@ -412,14 +414,15 @@ paths: "@context": rdfs: http://www.w3.org/2000/01/rdf-schema# skos: http://www.w3.org/2004/02/skos/core# - dfc: http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl# + dfc: https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_FullModel.owl# dc: http://purl.org/dc/elements/1.1/# - dfc-b: http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl# - dfc-p: http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl# - dfc-t: http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl# - dfc-m: http://static.datafoodconsortium.org/data/measures.rdf# - dfc-pt: http://static.datafoodconsortium.org/data/productTypes.rdf# - dfc-f: http://static.datafoodconsortium.org/data/facets.rdf# + dfc-b: https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl# + dfc-p: https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_ProductGlossary.owl# + dfc-t: https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_TechnicalOntology.owl# + dfc-m: https://github.com/datafoodconsortium/taxonomies/releases/latest/download/measures.rdf# + dfc-pt: https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf# + dfc-f: https://github.com/datafoodconsortium/taxonomies/releases/latest/download/facets.rdf# + ontosec: http://www.semanticweb.org/ontologies/2008/11/OntologySecurity.owl# dfc-p:hasUnit: "@type": "@id" dfc-b:hasUnit: @@ -442,6 +445,8 @@ paths: "@type": "@id" dfc-b:affiliates: "@type": "@id" + dfc-b:hasCertification: + "@type": "@id" dfc-b:manages: "@type": "@id" dfc-b:offeredThrough: @@ -454,7 +459,7 @@ paths: "@type": "@id" dfc-b:hasAllergenDimension: "@type": "@id" - dfc-b:hasNutrimentDimension: + dfc-b:hasNutrientDimension: "@type": "@id" dfc-b:hasPhysicalDimension: "@type": "@id" @@ -466,7 +471,22 @@ paths: "@type": "@id" dfc-t:represent: "@type": "@id" + "@id": https://staging.coopcircuits.fr/api/dfc-v1.7/enterprises/2731/supplied_products/56790 + "@type": dfc-b:SuppliedProduct + dfc-b:alcoholPercentage: 0 dfc-b:description: DFC-Pesto updated - dfc-b:quantity: 17 + dfc-b:hasQuantity: + "@type": dfc-b:QuantitativeValue + dfc-b:hasUnit: dfc-m:Piece + dfc-b:value: 17 + dfc-b:hasType: dfc-pt:non-local-vegetable + dfc-b:lifetime: '' + dfc-b:name: Pesto novo + dfc-b:totalTheoreticalStock: 0 + dfc-b:usageOrStorageCondition: '' + dfc:owner: http://proto.datafoodconsortium.org:3000/ldp/user/64c1d30351ecb4367037a9f6 + dfc-b:hasPhysicalCharacteristic: [] + dfc-b:hasNutrientCharacteristic: [] + dfc-b:hasAllergenCharacteristic: [] servers: - url: "/"