forked from openfoodfoundation/openfoodnetwork
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request openfoodfoundation#12831 from mkllnk/anonymous-orders
Share anonymised sales data on DFC API with authorised users
- Loading branch information
Showing
13 changed files
with
575 additions
and
3 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
engines/dfc_provider/app/controllers/dfc_provider/affiliate_sales_data_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# frozen_string_literal: true | ||
|
||
module DfcProvider | ||
# Aggregates anonymised sales data for a research project. | ||
class AffiliateSalesDataController < DfcProvider::ApplicationController | ||
rescue_from Date::Error, with: -> { head :bad_request } | ||
|
||
def show | ||
person = AffiliateSalesDataBuilder.person(current_user, filter_params) | ||
|
||
render json: DfcIo.export(person) | ||
end | ||
|
||
private | ||
|
||
def filter_params | ||
{ | ||
start_date: parse_date(params[:startDate]), | ||
end_date: parse_date(params[:endDate]), | ||
} | ||
end | ||
|
||
def parse_date(string) | ||
return if string.blank? | ||
|
||
Date.parse(string) | ||
end | ||
end | ||
end |
17 changes: 17 additions & 0 deletions
17
engines/dfc_provider/app/services/affiliate_sales_data_builder.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# frozen_string_literal: true | ||
|
||
class AffiliateSalesDataBuilder < DfcBuilder | ||
class << self | ||
def person(user, filters = {}) | ||
data = AffiliateSalesQuery.data(user.affiliate_enterprises, **filters) | ||
suppliers = data.map do |row| | ||
AffiliateSalesDataRowBuilder.new(row).build_supplier | ||
end | ||
|
||
DataFoodConsortium::Connector::Person.new( | ||
urls.affiliate_sales_data_url, | ||
affiliatedOrganizations: suppliers, | ||
) | ||
end | ||
end | ||
end |
98 changes: 98 additions & 0 deletions
98
engines/dfc_provider/app/services/affiliate_sales_data_row_builder.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
90 changes: 90 additions & 0 deletions
90
engines/dfc_provider/app/services/affiliate_sales_query.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# frozen_string_literal: true | ||
|
||
class AffiliateSalesQuery | ||
class << self | ||
def data(enterprises, start_date: nil, end_date: nil) | ||
end_date = end_date&.end_of_day # Include the whole end date. | ||
|
||
Spree::LineItem | ||
.joins(tables) | ||
.where( | ||
spree_orders: { | ||
state: "complete", distributor_id: enterprises, | ||
completed_at: [start_date..end_date], | ||
}, | ||
) | ||
.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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
61 changes: 61 additions & 0 deletions
61
engines/dfc_provider/spec/requests/affiliate_sales_data_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "../swagger_helper" | ||
|
||
RSpec.describe "AffiliateSalesData", swagger_doc: "dfc.yaml", rswag_autodoc: true do | ||
let(:user) { create(:oidc_user) } | ||
|
||
before { login_as user } | ||
|
||
path "/api/dfc/affiliate_sales_data" do | ||
parameter name: :startDate, in: :query, type: :string | ||
parameter name: :endDate, in: :query, type: :string | ||
|
||
get "Show sales data of person's affiliate enterprises" do | ||
produces "application/json" | ||
|
||
response "200", "successful", feature: :affiliate_sales_data do | ||
let(:startDate) { Date.yesterday } | ||
let(:endDate) { Time.zone.today } | ||
|
||
before do | ||
order = create(:order_with_totals_and_distribution, :completed) | ||
ConnectedApps::AffiliateSalesData.new( | ||
enterprise: order.distributor | ||
).connect({}) | ||
end | ||
|
||
context "with date filters" do | ||
let(:startDate) { Date.tomorrow } | ||
let(:endDate) { Date.tomorrow } | ||
|
||
run_test! do | ||
expect(json_response).to include( | ||
"@id" => "http://test.host/api/dfc/affiliate_sales_data", | ||
"@type" => "dfc-b:Person", | ||
) | ||
|
||
expect(json_response["dfc-b:affiliates"]).to eq nil | ||
end | ||
end | ||
|
||
context "not filtered" do | ||
run_test! do | ||
expect(json_response).to include( | ||
"@id" => "http://test.host/api/dfc/affiliate_sales_data", | ||
"@type" => "dfc-b:Person", | ||
) | ||
expect(json_response["dfc-b:affiliates"]).to be_present | ||
end | ||
end | ||
end | ||
|
||
response "400", "bad request" do | ||
let(:startDate) { "yesterday" } | ||
let(:endDate) { "tomorrow" } | ||
|
||
run_test! | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.