mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-26 01:33:22 +00:00
Build DFC data for sales
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
85
engines/dfc_provider/app/services/affiliate_sales_query.rb
Normal file
85
engines/dfc_provider/app/services/affiliate_sales_query.rb
Normal file
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
28
engines/dfc_provider/lib/dfc_provider/coordination.rb
Normal file
28
engines/dfc_provider/lib/dfc_provider/coordination.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user