diff --git a/app/controllers/cart_controller.rb b/app/controllers/cart_controller.rb index c8d246ee21..5eadc172d1 100644 --- a/app/controllers/cart_controller.rb +++ b/app/controllers/cart_controller.rb @@ -11,6 +11,8 @@ class CartController < BaseController order.cap_quantity_at_stock! order.recreate_all_fees! + StockSyncJob.sync_linked_catalogs(order) + render json: { error: false, stock_levels: stock_levels(order) }, status: :ok else render json: { error: cart_service.errors.full_messages.join(",") }, diff --git a/app/jobs/stock_sync_job.rb b/app/jobs/stock_sync_job.rb new file mode 100644 index 0000000000..9359aeb81e --- /dev/null +++ b/app/jobs/stock_sync_job.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class StockSyncJob < ApplicationJob + # No retry but stay as failed job: + sidekiq_options retry: 0 + + # We synchronise stock of stock-controlled variants linked to a remote + # product. These variants are rare though and we check first before we + # enqueue a new job. That should save some time loading the order with + # all the stock data to make this decision. + def self.sync_linked_catalogs(order) + stock_controlled_variants = order.variants.reject(&:on_demand) + links = SemanticLink.where(variant_id: stock_controlled_variants.map(&:id)) + semantic_ids = links.pluck(:semantic_id) + + return if semantic_ids.empty? + + user = order.distributor.owner + reference_id = semantic_ids.first # Assuming one catalog for now. + perform_later(user, reference_id) + rescue StandardError => e + # Errors here shouldn't affect the shopping. So let's report them + # separately: + Bugsnag.notify(e) do |payload| + payload.add_metadata(:order, order) + end + end + + def perform(user, semantic_id) + urls = FdcUrlBuilder.new(semantic_id) + json_catalog = DfcRequest.new(user).call(urls.catalog_url) + graph = DfcIo.import(json_catalog) + + products = graph.select do |subject| + subject.is_a? DataFoodConsortium::Connector::SuppliedProduct + end + products_by_id = products.index_by(&:semanticId) + product_ids = products_by_id.keys + variants = Spree::Variant.where(supplier: user.enterprises) + .includes(:semantic_links).references(:semantic_links) + .where(semantic_links: { semantic_id: product_ids }) + + variants.each do |variant| + next if variant.on_demand + + product = products_by_id[variant.semantic_links[0].semantic_id] + catalog_item = product&.catalogItems&.first + CatalogItemBuilder.apply_stock(catalog_item, variant) + variant.stock_items[0].save! + end + end +end diff --git a/spec/jobs/stock_sync_job_spec.rb b/spec/jobs/stock_sync_job_spec.rb new file mode 100644 index 0000000000..2abc9eb85a --- /dev/null +++ b/spec/jobs/stock_sync_job_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe StockSyncJob do + let(:order) { create(:order_with_totals, distributor:) } + let(:distributor) { build(:enterprise, owner: user) } + let(:user) { build(:testdfc_user) } + let(:beans) { order.variants.first } + let(:beans_retail_link) { + "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635" + } + + describe ".sync_linked_catalogs" do + subject { StockSyncJob.sync_linked_catalogs(order) } + it "ignores products without semantic link" do + expect { subject }.not_to enqueue_job(StockSyncJob) + end + + it "enqueues backorder" do + beans.semantic_links << SemanticLink.new( + semantic_id: beans_retail_link + ) + + expect { subject }.to enqueue_job(StockSyncJob) + .with(user, beans_retail_link) + end + + it "reports errors" do + expect(order).to receive(:variants).and_raise("test error") + expect(Bugsnag).to receive(:notify).and_call_original + + expect { subject }.not_to raise_error + end + end + + describe "#peform" do + subject { StockSyncJob.perform_now(user, beans_retail_link) } + + before do + distributor.save! + user.enterprises << distributor + beans.update!(supplier: distributor) + beans.semantic_links << SemanticLink.new(semantic_id: beans_retail_link) + end + + it "updates stock" do + expect { VCR.use_cassette(:fdc_catalog) { subject } }.to change { + beans.on_demand + }.from(false).to(true) + end + end +end