diff --git a/engines/dfc_provider/app/controllers/dfc_provider/affiliate_sales_data_controller.rb b/engines/dfc_provider/app/controllers/dfc_provider/affiliate_sales_data_controller.rb index 73b09af76c..1e316f37b5 100644 --- a/engines/dfc_provider/app/controllers/dfc_provider/affiliate_sales_data_controller.rb +++ b/engines/dfc_provider/app/controllers/dfc_provider/affiliate_sales_data_controller.rb @@ -4,7 +4,7 @@ module DfcProvider # Aggregates anonymised sales data for a research project. class AffiliateSalesDataController < DfcProvider::ApplicationController def show - person = AffiliateSalesDataBuilder.person + person = AffiliateSalesDataBuilder.person(current_user) render json: DfcIo.export(person) end diff --git a/engines/dfc_provider/app/services/affiliate_sales_data_builder.rb b/engines/dfc_provider/app/services/affiliate_sales_data_builder.rb index 1b2e99ec3c..74a403a2f8 100644 --- a/engines/dfc_provider/app/services/affiliate_sales_data_builder.rb +++ b/engines/dfc_provider/app/services/affiliate_sales_data_builder.rb @@ -2,10 +2,17 @@ class AffiliateSalesDataBuilder < DfcBuilder class << self - def person + def person(user) DataFoodConsortium::Connector::Person.new( urls.affiliate_sales_data_url, + affiliatedOrganizations: enterprises(user.enterprises) ) end + + def enterprises(enterprises) + AffiliateSalesQuery.data(enterprises).map do |row| + AffiliateSalesDataRowBuilder.new(row).build_supplier + end + end end end diff --git a/engines/dfc_provider/app/services/affiliate_sales_data_row_builder.rb b/engines/dfc_provider/app/services/affiliate_sales_data_row_builder.rb new file mode 100644 index 0000000000..073097a04f --- /dev/null +++ b/engines/dfc_provider/app/services/affiliate_sales_data_row_builder.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# Represents a single row of the aggregated sales data. +class AffiliateSalesDataRowBuilder < DfcBuilder + attr_reader :item + + def initialize(row) + super() + @item = AffiliateSalesQuery.label_row(row) + end + + def build_supplier + DataFoodConsortium::Connector::Enterprise.new( + nil, + localizations: [build_address(item[:supplier_postcode])], + suppliedProducts: [build_product], + ) + end + + def build_distributor + DataFoodConsortium::Connector::Enterprise.new( + nil, + localizations: [build_address(item[:distributor_postcode])], + ) + end + + def build_product + DataFoodConsortium::Connector::SuppliedProduct.new( + nil, + name: item[:product_name], + quantity: build_product_quantity, + ).tap do |product| + product.registerSemanticProperty("dfc-b:concernedBy") { + build_order_line + } + end + end + + def build_order_line + DataFoodConsortium::Connector::OrderLine.new( + nil, + quantity: build_line_quantity, + price: build_price, + order: build_order, + ) + end + + def build_order + DataFoodConsortium::Connector::Order.new( + nil, + saleSession: build_sale_session, + ) + end + + def build_sale_session + DataFoodConsortium::Connector::SaleSession.new( + nil, + ).tap do |session| + session.registerSemanticProperty("dfc-b:objectOf") { + build_coordination + } + end + end + + def build_coordination + DfcProvider::Coordination.new( + nil, + coordinator: build_distributor, + ) + end + + def build_product_quantity + DataFoodConsortium::Connector::QuantitativeValue.new( + unit: QuantitativeValueBuilder.unit(item[:unit_type]), + value: item[:units]&.to_f, + ) + end + + def build_line_quantity + DataFoodConsortium::Connector::QuantitativeValue.new( + unit: DfcLoader.connector.MEASURES.PIECE, + value: item[:quantity_sold]&.to_f, + ) + end + + def build_price + DataFoodConsortium::Connector::QuantitativeValue.new( + value: item[:price]&.to_f, + ) + end + + def build_address(postcode) + DataFoodConsortium::Connector::Address.new( + nil, + postalCode: postcode, + ) + end +end diff --git a/engines/dfc_provider/app/services/affiliate_sales_query.rb b/engines/dfc_provider/app/services/affiliate_sales_query.rb new file mode 100644 index 0000000000..f85333f4d8 --- /dev/null +++ b/engines/dfc_provider/app/services/affiliate_sales_query.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +class AffiliateSalesQuery + class << self + def data(enterprises) + Spree::LineItem + .joins(tables) + .where( + spree_orders: { state: "complete", distributor_id: enterprises }, + ) + .group(key_fields) + .pluck(fields) + end + + # Create a hash with labels for an array of data points: + # + # { product_name: "Apple", ... } + def label_row(row) + labels.zip(row).to_h + end + + private + + # We want to collect a lot of data from only a few columns. + # It's more efficient with `pluck`. But therefore we need well named + # tables and columns, especially because we are going to join some tables + # twice for different columns. For example the distributer postcode and + # the supplier postcode. That's why we need SQL here instead of nice Rails + # associations. + def tables + <<~SQL.squish + JOIN spree_variants ON spree_variants.id = spree_line_items.variant_id + JOIN spree_products ON spree_products.id = spree_variants.product_id + JOIN enterprises AS suppliers ON suppliers.id = spree_variants.supplier_id + JOIN spree_addresses AS supplier_addresses ON supplier_addresses.id = suppliers.address_id + JOIN spree_orders ON spree_orders.id = spree_line_items.order_id + JOIN enterprises AS distributors ON distributors.id = spree_orders.distributor_id + JOIN spree_addresses AS distributor_addresses ON distributor_addresses.id = distributors.address_id + SQL + end + + def fields + <<~SQL.squish + spree_products.name AS product_name, + spree_variants.display_name AS unit_name, + spree_products.variant_unit AS unit_type, + spree_variants.unit_value AS units, + spree_variants.unit_presentation, + spree_line_items.price, + distributor_addresses.zipcode AS distributor_postcode, + supplier_addresses.zipcode AS supplier_postcode, + + SUM(spree_line_items.quantity) AS quantity_sold + SQL + end + + def key_fields + <<~SQL.squish + product_name, + unit_name, + unit_type, + units, + spree_variants.unit_presentation, + spree_line_items.price, + distributor_postcode, + supplier_postcode + SQL + end + + # A list of column names as symbols to be used as hash keys. + def labels + %i[ + product_name + unit_name + unit_type + units + unit_presentation + price + distributor_postcode + supplier_postcode + quantity_sold + ] + end + end +end diff --git a/engines/dfc_provider/app/services/quantitative_value_builder.rb b/engines/dfc_provider/app/services/quantitative_value_builder.rb index 6baac1eecf..ecbc0f4c59 100644 --- a/engines/dfc_provider/app/services/quantitative_value_builder.rb +++ b/engines/dfc_provider/app/services/quantitative_value_builder.rb @@ -11,13 +11,13 @@ class QuantitativeValueBuilder < DfcBuilder def self.quantity(variant) DataFoodConsortium::Connector::QuantitativeValue.new( - unit: unit(variant), + unit: unit(variant.product.variant_unit), value: variant.unit_value, ) end - def self.unit(variant) - case variant.product.variant_unit + def self.unit(unit_name) + case unit_name when "volume" DfcLoader.connector.MEASURES.LITRE when "weight" diff --git a/engines/dfc_provider/lib/dfc_provider.rb b/engines/dfc_provider/lib/dfc_provider.rb index 526cceb9c3..51f3ec2ef4 100644 --- a/engines/dfc_provider/lib/dfc_provider.rb +++ b/engines/dfc_provider/lib/dfc_provider.rb @@ -9,6 +9,7 @@ require "dfc_provider/engine" # Custom data types require "dfc_provider/supplied_product" require "dfc_provider/address" +require "dfc_provider/coordination" module DfcProvider DataFoodConsortium::Connector::Importer.register_type(SuppliedProduct) diff --git a/engines/dfc_provider/lib/dfc_provider/coordination.rb b/engines/dfc_provider/lib/dfc_provider/coordination.rb new file mode 100644 index 0000000000..49b16aa97a --- /dev/null +++ b/engines/dfc_provider/lib/dfc_provider/coordination.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +if defined? DataFoodConsortium::Connector::Coordination + ActiveSupport::Deprecation.warn <<~TEXT + DataFoodConsortium::Connector::Coordination is now available. + Please replace your own implementation with the official class. + TEXT +end + +module DfcProvider + class Coordination + include VirtualAssembly::Semantizer::SemanticObject + + SEMANTIC_TYPE = "dfc-b:Coordination" + + attr_accessor :coordinator + + def initialize(semantic_id, coordinator: nil) + super(semantic_id) + + self.semanticType = SEMANTIC_TYPE + + @coordinator = coordinator + registerSemanticProperty("dfc-b:coordinatedBy", &method("coordinator")) + .valueSetter = method("coordinator=") + end + end +end diff --git a/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb b/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb index cd8ef1cc54..123b575d44 100644 --- a/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb +++ b/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb @@ -13,42 +13,67 @@ RSpec.describe AffiliateSalesDataBuilder do expect(person.semanticId).to eq "http://test.host/api/dfc/affiliate_sales_data" end - it "returns required sales data" do - supplier = create( - :supplier_enterprise, - owner: user, - users: [user], - address: create(:address, zipcode: "5555"), - ) - product = create( - :product, - supplier_id: supplier.id, - variant_unit: "item", - ) - variant = product.variants.first - distributor = create( - :distributor_enterprise, - address: create(:address, zipcode: "6666"), - ) - line_item = build( - :line_item, - variant:, - quantity: 2, - price: 3, - ) - order_cycle = create( - :order_cycle, - suppliers: [supplier], - distributors: [distributor], - ) - order_cycle.exchanges.incoming.first.variants << variant - order_cycle.exchanges.outgoing.first.variants << variant - create( - :order, - order_cycle:, - distributor:, - line_items: [line_item], - ) + describe "with sales data" do + before do + supplier = create( + :supplier_enterprise, + owner: user, + users: [user], + address: create(:address, zipcode: "5555"), + ) + product = create( + :product, + name: "Pomme", + supplier_id: supplier.id, + variant_unit: "item", + ) + variant = product.variants.first + distributor = create( + :distributor_enterprise, + owner: user, + address: create(:address, zipcode: "6666"), + ) + line_item = build( + :line_item, + variant:, + quantity: 2, + price: 3, + ) + order_cycle = create( + :order_cycle, + suppliers: [supplier], + distributors: [distributor], + ) + order_cycle.exchanges.incoming.first.variants << variant + order_cycle.exchanges.outgoing.first.variants << variant + create( + :order, + state: "complete", + order_cycle:, + distributor:, + line_items: [line_item], + ) + end + + it "returns required sales data" do + supplier = person.affiliatedOrganizations[0] + product = supplier.suppliedProducts[0] + line = product.semanticPropertyValue("dfc-b:concernedBy") + session = line.order.saleSession + coordination = session.semanticPropertyValue("dfc-b:objectOf") + distributor = coordination.coordinator + + expect(supplier.localizations[0].postalCode).to eq "5555" + expect(distributor.localizations[0].postalCode).to eq "6666" + + expect(product.name).to eq "Pomme" + expect(product.quantity.unit).to eq DfcLoader.connector.MEASURES.PIECE + expect(product.quantity.value).to eq 1 + + expect(line.quantity.unit).to eq DfcLoader.connector.MEASURES.PIECE + expect(line.quantity.value).to eq 2 + expect(line.price.value).to eq 3 + end end end end diff --git a/engines/dfc_provider/spec/services/affiliate_sales_query_spec.rb b/engines/dfc_provider/spec/services/affiliate_sales_query_spec.rb new file mode 100644 index 0000000000..1cadb16110 --- /dev/null +++ b/engines/dfc_provider/spec/services/affiliate_sales_query_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe AffiliateSalesQuery do + subject(:query) { described_class } + + describe ".label_row" do + it "converts an array to a hash" do + row = [ + "Apples", + "item", "item", nil, nil, + 15.50, + "3210", "3211", + 3, + ] + expect(query.label_row(row)).to eq( + { + product_name: "Apples", + unit_name: "item", + unit_type: "item", + units: nil, + unit_presentation: nil, + price: 15.50, + distributor_postcode: "3210", + supplier_postcode: "3211", + quantity_sold: 3, + } + ) + end + end +end diff --git a/swagger/dfc.yaml b/swagger/dfc.yaml index d302047b7c..3b1e904225 100644 --- a/swagger/dfc.yaml +++ b/swagger/dfc.yaml @@ -88,12 +88,6 @@ paths: dfc-b:logo: '' dfc-b:firstName: '' dfc-b:familyName: '' - dfc-b:affiliates: - "@type": dfc-b:Enterprise - dfc-b:logo: '' - dfc-b:name: '' - dfc-b:hasDescription: '' - dfc-b:VATnumber: '' "/api/dfc/enterprises/{enterprise_id}/catalog_items": parameters: - name: enterprise_id