diff --git a/app/controllers/admin/dfc_product_imports_controller.rb b/app/controllers/admin/dfc_product_imports_controller.rb index ab3b819c4f2..58239786241 100644 --- a/app/controllers/admin/dfc_product_imports_controller.rb +++ b/app/controllers/admin/dfc_product_imports_controller.rb @@ -19,15 +19,12 @@ def index .find(params.require(:enterprise_id)) catalog_url = params.require(:catalog_url) - broker = FdcOfferBroker.new(spree_current_user, catalog_url) + catalog = DfcCatalog.load(spree_current_user, catalog_url) + catalog.apply_wholesale_values! # * First step: import all products for given enterprise. # * Second step: render table and let user decide which ones to import. - imported = broker.catalog.map do |subject| - next unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct - - adjust_to_wholesale_price(broker, subject) - + imported = catalog.products.map do |subject| existing_variant = enterprise.supplied_variants.linked_to(subject.semanticId) if existing_variant @@ -44,24 +41,5 @@ def index flash[:error] = e.message redirect_to admin_product_import_path end - - private - - def adjust_to_wholesale_price(broker, product) - transformation = broker.best_offer(product.semanticId) - - return if transformation.factor == 1 - - wholesale_variant_price = transformation.offer.price - - return unless wholesale_variant_price - - offer = product.catalogItems&.first&.offers&.first - - return unless offer - - offer.price = wholesale_variant_price.dup - offer.price.value = offer.price.value.to_f / transformation.factor - end end end diff --git a/app/jobs/backorder_job.rb b/app/jobs/backorder_job.rb index c2a0b455272..a0a3c168e50 100644 --- a/app/jobs/backorder_job.rb +++ b/app/jobs/backorder_job.rb @@ -117,7 +117,8 @@ def needed_quantity(line_item) end def load_broker(user, urls) - FdcOfferBroker.new(user, urls.catalog_url) + catalog = DfcCatalog.load(user, urls.catalog_url) + FdcOfferBroker.new(catalog) end def place_order(user, order, orderer, backorder) diff --git a/app/jobs/stock_sync_job.rb b/app/jobs/stock_sync_job.rb index d4df9c3c613..565d6425d4a 100644 --- a/app/jobs/stock_sync_job.rb +++ b/app/jobs/stock_sync_job.rb @@ -40,7 +40,10 @@ def self.catalog_ids(order) end def perform(user, catalog_id) - products = load_products(user, catalog_id) + catalog = DfcCatalog.load(user, catalog_id) + catalog.apply_wholesale_values! + + products = catalog.products products_by_id = products.index_by(&:semanticId) product_ids = products_by_id.keys variants = linked_variants(user.enterprises, product_ids) @@ -58,15 +61,6 @@ def perform(user, catalog_id) end end - def load_products(user, catalog_id) - json_catalog = DfcRequest.new(user).call(catalog_id) - graph = DfcIo.import(json_catalog) - - graph.select do |subject| - subject.is_a? DataFoodConsortium::Connector::SuppliedProduct - end - end - def linked_variants(enterprises, product_ids) Spree::Variant.where(supplier: enterprises) .includes(:semantic_links).references(:semantic_links) diff --git a/app/services/backorder_updater.rb b/app/services/backorder_updater.rb index e57e59ff6c3..bb163c8be0a 100644 --- a/app/services/backorder_updater.rb +++ b/app/services/backorder_updater.rb @@ -40,7 +40,8 @@ def update(backorder, user, distributor, order_cycle) reference_link = variants[0].semantic_links[0].semantic_id urls = FdcUrlBuilder.new(reference_link) orderer = FdcBackorderer.new(user, urls) - broker = FdcOfferBroker.new(user, urls.catalog_url) + catalog = DfcCatalog.load(user, urls.catalog_url) + broker = FdcOfferBroker.new(catalog) updated_lines = update_order_lines(backorder, order_cycle, variants, broker, orderer) unprocessed_lines = backorder.lines.to_set - updated_lines diff --git a/app/services/fdc_offer_broker.rb b/app/services/fdc_offer_broker.rb index c4108ee26c5..5071028eaca 100644 --- a/app/services/fdc_offer_broker.rb +++ b/app/services/fdc_offer_broker.rb @@ -6,19 +6,10 @@ class FdcOfferBroker Solution = Struct.new(:product, :factor, :offer) RetailSolution = Struct.new(:retail_product_id, :factor) - def self.load_catalog(user, catalog_url) - api = DfcRequest.new(user) - catalog_json = api.call(catalog_url) - DfcIo.import(catalog_json) - end - - def initialize(user, catalog_url) - @user = user - @catalog_url = catalog_url - end + attr_reader :catalog - def catalog - @catalog ||= self.class.load_catalog(@user, @catalog_url) + def initialize(catalog) + @catalog = catalog end def best_offer(product_id) @@ -30,18 +21,18 @@ def best_offer(product_id) end def wholesale_product(product_id) - production_flow = catalog_item("#{product_id}/AsPlannedProductionFlow") + production_flow = catalog.item("#{product_id}/AsPlannedProductionFlow") if production_flow production_flow.product else # We didn't find a wholesale variant, falling back to the given product. - catalog_item(product_id) + catalog.item(product_id) end end def contained_quantity(product_id) - consumption_flow = catalog_item("#{product_id}/AsPlannedConsumptionFlow") + consumption_flow = catalog.item("#{product_id}/AsPlannedConsumptionFlow") # If we don't find a transformation, we return the original product, # which contains exactly one of itself (identity). @@ -53,7 +44,7 @@ def wholesale_to_retail(wholesale_product_id) return RetailSolution.new(wholesale_product_id, 1) if production_flow.nil? - consumption_flow = catalog_item( + consumption_flow = catalog.item( production_flow.semanticId.sub("AsPlannedProductionFlow", "AsPlannedConsumptionFlow") ) retail_product_id = consumption_flow.product.semanticId @@ -70,19 +61,12 @@ def offer_of(product) end end - def catalog_item(id) - @catalog_by_id ||= catalog.index_by(&:semanticId) - @catalog_by_id[id] - end - def flow_producing(wholesale_product_id) @production_flows_by_product_id ||= production_flows.index_by { |flow| flow.product.semanticId } @production_flows_by_product_id[wholesale_product_id] end def production_flows - @production_flows ||= catalog.select do |i| - i.semanticType == "dfc-b:AsPlannedProductionFlow" - end + @production_flows ||= catalog.select_type("dfc-b:AsPlannedProductionFlow") end end diff --git a/engines/dfc_provider/app/services/dfc_catalog.rb b/engines/dfc_provider/app/services/dfc_catalog.rb new file mode 100644 index 00000000000..f068bdc3cf8 --- /dev/null +++ b/engines/dfc_provider/app/services/dfc_catalog.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +class DfcCatalog + def self.load(user, catalog_url) + api = DfcRequest.new(user) + catalog_json = api.call(catalog_url) + graph = DfcIo.import(catalog_json) + + new(graph) + end + + def initialize(graph) + @graph = graph + end + + def products + @products ||= @graph.select do |subject| + subject.is_a? DataFoodConsortium::Connector::SuppliedProduct + end + end + + def item(semantic_id) + @items ||= @graph.index_by(&:semanticId) + @items[semantic_id] + end + + def select_type(semantic_type) + @graph.select { |i| i.semanticType == semantic_type } + end + + def apply_wholesale_values! + broker = FdcOfferBroker.new(self) + products.each do |product| + transformation = broker.best_offer(product.semanticId) + + next if transformation.factor == 1 + + adjust_to_wholesale_price(product, transformation) + adjust_to_wholesale_stock(product, transformation) + end + end + + private + + def adjust_to_wholesale_price(product, transformation) + wholesale_variant_price = transformation.offer.price + + return unless wholesale_variant_price + + offer = product.catalogItems&.first&.offers&.first + + return unless offer + + offer.price = wholesale_variant_price.dup + offer.price.value = offer.price.value.to_f / transformation.factor + end + + def adjust_to_wholesale_stock(product, transformation) + adjust_item_stock(product, transformation) + adjust_offer_stock(product, transformation) + end + + def adjust_item_stock(product, transformation) + item = product.catalogItems&.first + wholesale_item = transformation.product.catalogItems&.first + + return unless item && wholesale_item&.stockLimitation.present? + + item.stockLimitation = wholesale_item.stockLimitation.to_i * transformation.factor + end + + def adjust_offer_stock(product, transformation) + offer = product.catalogItems&.first&.offers&.first + wholesale_offer = transformation.offer + + return unless offer && wholesale_offer&.stockLimitation.present? + + offer.stockLimiation = wholesale_offer.stockLimitation.to_i * transformation.factor + end +end diff --git a/engines/dfc_provider/spec/services/dfc_catalog_spec.rb b/engines/dfc_provider/spec/services/dfc_catalog_spec.rb new file mode 100644 index 00000000000..15e59f1d2ec --- /dev/null +++ b/engines/dfc_provider/spec/services/dfc_catalog_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +RSpec.describe DfcCatalog do + subject(:catalog) { + VCR.use_cassette(:fdc_catalog) { + DfcCatalog.load(user, catalog_url) + } + } + let(:user) { build(:testdfc_user) } + let(:catalog_url) { + "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" + } + + describe "#products" do + let(:products) { catalog.products } + + it "returns only products" do + expect(products.count).to eq 4 + expect(products.map(&:semanticType).uniq).to eq ["dfc-b:SuppliedProduct"] + end + end + + describe "#apply_wholesale_values!" do + let(:offer) { beans.catalogItems.first.offers.first } + let(:catalog_item) { beans.catalogItems.first } + let(:beans) { catalog.item(beans_id) } + let(:beans_id) { + "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635" + } + + it "changes price of retail variants" do + expect { catalog.apply_wholesale_values! }.to change { + offer.price.value.to_f.round(2) + }.from(2.09).to(1.57) # 18.85 wholesale price divided by 12 + end + + it "changes stock level of retail variants" do + expect { catalog.apply_wholesale_values! }.to change { + catalog_item.stockLimitation + }.from("-1").to(-12) + end + end +end diff --git a/spec/jobs/complete_backorder_job_spec.rb b/spec/jobs/complete_backorder_job_spec.rb index ad6479079f9..0bc1a741e36 100644 --- a/spec/jobs/complete_backorder_job_spec.rb +++ b/spec/jobs/complete_backorder_job_spec.rb @@ -14,13 +14,14 @@ let(:orderer) { FdcBackorderer.new(user, urls) } let(:order) { backorder = orderer.find_or_build_order(ofn_order) - broker = FdcOfferBroker.new(user, urls.catalog_url) + catalog = DfcCatalog.load(user, urls.catalog_url) + broker = FdcOfferBroker.new(catalog) bean_offer = broker.best_offer(product_link).offer bean_line = orderer.find_or_build_order_line(backorder, bean_offer) bean_line.quantity = 3 - chia = broker.catalog_item(chia_seed_retail_link) + chia = catalog.item(chia_seed_retail_link) chia_offer = broker.offer_of(chia) chia_line = orderer.find_or_build_order_line(backorder, chia_offer) chia_line.quantity = 5 diff --git a/spec/services/fdc_backorderer_spec.rb b/spec/services/fdc_backorderer_spec.rb index 8e1e828a04d..8b243038b3d 100644 --- a/spec/services/fdc_backorderer_spec.rb +++ b/spec/services/fdc_backorderer_spec.rb @@ -33,9 +33,9 @@ expect(backorder.lines).to eq [] # Add items and place the new order: - catalog = FdcOfferBroker.load_catalog(order.distributor.owner, urls.catalog_url) - product = catalog.find { |i| i.semanticType == "dfc-b:SuppliedProduct" } - offer = FdcOfferBroker.new(nil, nil).offer_of(product) + catalog = DfcCatalog.load(order.distributor.owner, urls.catalog_url) + product = catalog.products.first + offer = FdcOfferBroker.new(nil).offer_of(product) line = subject.find_or_build_order_line(backorder, offer) line.quantity = 3 placed_order = subject.send_order(backorder) @@ -80,7 +80,7 @@ describe "#find_or_build_order_line" do it "add quantity to an existing line item", vcr: true do - catalog = FdcOfferBroker.load_catalog(order.distributor.owner, urls.catalog_url) + catalog = DfcCatalog.load(order.distributor.owner, urls.catalog_url) backorder = subject.find_or_build_order(order) existing_line = backorder.lines[0] @@ -89,10 +89,8 @@ # the catalog offer here which is different to the offer on the # existing order line. ordered_product = existing_line.offer.offeredItem - catalog_product = catalog.find do |i| - i.semanticId == ordered_product.semanticId - end - catalog_offer = FdcOfferBroker.new(nil, nil).offer_of(catalog_product) + catalog_product = catalog.item(ordered_product.semanticId) + catalog_offer = FdcOfferBroker.new(nil).offer_of(catalog_product) # The API response is missing this connection: catalog_offer.offeredItem = catalog_product diff --git a/spec/services/fdc_offer_broker_spec.rb b/spec/services/fdc_offer_broker_spec.rb index 403f3a84cfa..6987e0cec5c 100644 --- a/spec/services/fdc_offer_broker_spec.rb +++ b/spec/services/fdc_offer_broker_spec.rb @@ -3,9 +3,11 @@ require 'spec_helper' RSpec.describe FdcOfferBroker do - subject { FdcOfferBroker.new(user, catalog_url) } + subject { FdcOfferBroker.new(catalog) } let(:catalog) { - VCR.use_cassette(:fdc_catalog) { subject.catalog } + VCR.use_cassette(:fdc_catalog) { + DfcCatalog.load(user, catalog_url) + } } let(:catalog_url) { "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts" @@ -15,7 +17,7 @@ } let(:user) { build(:testdfc_user) } let(:product) { - catalog.find { |item| item.semanticType == "dfc-b:SuppliedProduct" } + catalog.products.first } describe ".best_offer" do