From 2297b650f8874777baaf6c28419b948c21cbf001 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 9 Jan 2025 12:10:07 +1100 Subject: [PATCH 1/5] Skip amending backorder if there's none --- app/services/backorder_updater.rb | 2 +- spec/services/backorder_updater_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/services/backorder_updater.rb b/app/services/backorder_updater.rb index bb163c8be0..e0a4ec8feb 100644 --- a/app/services/backorder_updater.rb +++ b/app/services/backorder_updater.rb @@ -28,7 +28,7 @@ class BackorderUpdater backorder = orderer.find_open_order(order) - update(backorder, user, distributor, order_cycle) + update(backorder, user, distributor, order_cycle) if backorder end # Update a given backorder according to a distributor's order cycle. diff --git a/spec/services/backorder_updater_spec.rb b/spec/services/backorder_updater_spec.rb index 48aa125fdc..3dace91076 100644 --- a/spec/services/backorder_updater_spec.rb +++ b/spec/services/backorder_updater_spec.rb @@ -107,6 +107,13 @@ RSpec.describe BackorderUpdater do .to change { backorder.lines.count }.from(2).to(1) .and change { beans.reload.on_hand }.by(-12) end + + it "skips updating if there's is no backorder" do + allow_any_instance_of(FdcBackorderer).to receive(:find_open_order) + .and_return(nil) + + expect { subject.amend_backorder(order) }.not_to raise_error + end end describe "#distributed_linked_variants" do From 884206b4ed92275e3ac84a936549a915f5c1c4f9 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 9 Jan 2025 13:28:09 +1100 Subject: [PATCH 2/5] Place new backorder when there's none to amend --- app/jobs/amend_backorder_job.rb | 14 +++++++++++--- spec/jobs/amend_backorder_job_spec.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/jobs/amend_backorder_job.rb b/app/jobs/amend_backorder_job.rb index b4d2bf0506..4b85beec15 100644 --- a/app/jobs/amend_backorder_job.rb +++ b/app/jobs/amend_backorder_job.rb @@ -23,8 +23,16 @@ class AmendBackorderJob < ApplicationJob def amend_backorder(order) backorder = BackorderUpdater.new.amend_backorder(order) - user = order.distributor.owner - urls = nil # Not needed to send order. The backorder id is the URL. - FdcBackorderer.new(user, urls).send_order(backorder) if backorder + if backorder + user = order.distributor.owner + urls = nil # Not needed to send order. The backorder id is the URL. + FdcBackorderer.new(user, urls).send_order(backorder) + elsif order.order_cycle.open? + + # We don't have an order to amend but the order cycle is open. + # We can assume that this job was triggered by an admin creating a new + # order or adding backorderable items to an order. + BackorderJob.new.place_backorder(order) + end end end diff --git a/spec/jobs/amend_backorder_job_spec.rb b/spec/jobs/amend_backorder_job_spec.rb index bde82b0741..3216b109da 100644 --- a/spec/jobs/amend_backorder_job_spec.rb +++ b/spec/jobs/amend_backorder_job_spec.rb @@ -130,5 +130,32 @@ RSpec.describe AmendBackorderJob do .to change { backorder.lines.count }.from(2).to(1) .and change { beans.reload.on_hand }.by(-12) end + + it "creates a new order" do + stub_request(:get, catalog_url).to_return(body: catalog_json) + + # Record the placed backorder: + backorder = nil + allow_any_instance_of(FdcBackorderer).to receive(:find_order) do |*_args| + backorder + end + allow_any_instance_of(FdcBackorderer).to receive(:send_order) do |*args| + backorder = args[1] + end + + # Call amending before a backorder has been placed. + expect { subject.amend_backorder(order) } + .to change { backorder.present? } + .to(true) + + # We ordered a case of 12 cans: -3 + 12 = 9 + expect(beans.on_hand).to eq 9 + + # Stock controlled items don't change stock in backorder: + expect(chia_seed.on_hand).to eq 7 + + expect(backorder.lines[0].quantity).to eq 1 # beans + expect(backorder.lines[1].quantity).to eq 5 # chia + end end end From dc7b6245fded8726ff160b8d457b2826c921e770 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 9 Jan 2025 13:32:43 +1100 Subject: [PATCH 3/5] Allow creating backorders before order cycle opens Admins may want to pre-process some orders manually for going public. And it's good to reserve stock for these. At some point in the future, the supplier may have an order cycle with its own times but the current FDC implementation allows orders at any time. --- app/jobs/amend_backorder_job.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/amend_backorder_job.rb b/app/jobs/amend_backorder_job.rb index 4b85beec15..883bed5328 100644 --- a/app/jobs/amend_backorder_job.rb +++ b/app/jobs/amend_backorder_job.rb @@ -27,9 +27,9 @@ class AmendBackorderJob < ApplicationJob user = order.distributor.owner urls = nil # Not needed to send order. The backorder id is the URL. FdcBackorderer.new(user, urls).send_order(backorder) - elsif order.order_cycle.open? + elsif !order.order_cycle.closed? - # We don't have an order to amend but the order cycle is open. + # We don't have an order to amend but the order cycle is or will open. # We can assume that this job was triggered by an admin creating a new # order or adding backorderable items to an order. BackorderJob.new.place_backorder(order) From c9eed4f5b84ab8687ea404dd2199b0c5c8432a78 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 15 Jan 2025 15:21:21 +1100 Subject: [PATCH 4/5] Trigger new backorder only when user checks out When an admin creates an order, then AmendBackorderJob is called which can also trigger a new backorder if needed. This means that we are not creating backorders via subscriptions any more. It has never been requested and we can bring that back if needed. --- app/controllers/checkout_controller.rb | 1 + app/models/spree/order.rb | 2 -- .../consumer/checkout/backorder_spec.rb | 23 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 spec/system/consumer/checkout/backorder_spec.rb diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index b41eeb5265..15cc8ef2f7 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -92,6 +92,7 @@ class CheckoutController < BaseController end @order.process_payments! @order.confirm! + BackorderJob.check_stock(@order) order_completion_reset @order end diff --git a/app/models/spree/order.rb b/app/models/spree/order.rb index 81bee889d3..10375b421d 100644 --- a/app/models/spree/order.rb +++ b/app/models/spree/order.rb @@ -392,8 +392,6 @@ module Spree deliver_order_confirmation_email - BackorderJob.check_stock(self) - state_changes.create( previous_state: 'cart', next_state: 'complete', diff --git a/spec/system/consumer/checkout/backorder_spec.rb b/spec/system/consumer/checkout/backorder_spec.rb new file mode 100644 index 0000000000..930e885a56 --- /dev/null +++ b/spec/system/consumer/checkout/backorder_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "system_helper" + +RSpec.describe "Checkout" do + include ShopWorkflow + include CheckoutHelper + + let(:variant) { order.variants.first } + let(:order) { create(:order_ready_for_confirmation) } + + before do + variant.semantic_links << SemanticLink.new(semantic_id: "https://product") + set_order order + login_as create(:user) + end + + it "triggers a backorder" do + visit checkout_step_path(:summary) + + expect { place_order }.to enqueue_job BackorderJob + end +end From 06d9d96f5462239f93571c990b34cd7e08cbee09 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 17 Jan 2025 12:21:28 +1100 Subject: [PATCH 5/5] Fix error on removed product from catalog --- app/services/backorder_updater.rb | 5 +++- spec/services/backorder_updater_spec.rb | 38 +++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/services/backorder_updater.rb b/app/services/backorder_updater.rb index e0a4ec8feb..10718296d0 100644 --- a/app/services/backorder_updater.rb +++ b/app/services/backorder_updater.rb @@ -58,6 +58,9 @@ class BackorderUpdater variants.map do |variant| link = variant.semantic_links[0].semantic_id solution = broker.best_offer(link) + + next unless solution.offer + line = orderer.find_or_build_order_line(backorder, solution.offer) if variant.on_demand adjust_stock(variant, solution, line) @@ -66,7 +69,7 @@ class BackorderUpdater end line - end + end.compact end def cancel_stale_lines(unprocessed_lines, managed_variants, broker) diff --git a/spec/services/backorder_updater_spec.rb b/spec/services/backorder_updater_spec.rb index 3dace91076..af1f5471c1 100644 --- a/spec/services/backorder_updater_spec.rb +++ b/spec/services/backorder_updater_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe BackorderUpdater do let(:order) { create(:completed_order_with_totals) } + let(:order_cycle) { order.order_cycle } let(:distributor) { order.distributor } let(:beans) { beans_item.variant } let(:beans_item) { order.line_items[0] } @@ -116,9 +117,42 @@ RSpec.describe BackorderUpdater do end end - describe "#distributed_linked_variants" do - let(:order_cycle) { order.order_cycle } + describe "#update_order_lines" do + it "skips unavailable items" do + stub_request(:get, catalog_url).to_return(body: catalog_json) + # Record the placed backorder: + backorder = nil + allow_any_instance_of(FdcBackorderer).to receive(:find_order) do |*_args| + backorder + end + allow_any_instance_of(FdcBackorderer).to receive(:send_order) do |*args| + backorder = args[1] + end + + BackorderJob.new.place_backorder(order) + + # Now one of the products becomes unavailable in the catalog. + # I simulate that by changing the link so something unknown. + beans.semantic_links[0].update!(semantic_id: "https://example.net/unknown") + + variants = [beans, chia_seed] + reference_link = chia_seed.semantic_links[0].semantic_id + urls = FdcUrlBuilder.new(reference_link) + catalog = DfcCatalog.load(user, urls.catalog_url) + orderer = FdcBackorderer.new(user, urls) + broker = FdcOfferBroker.new(catalog) + updated_lines = subject.update_order_lines( + backorder, order_cycle, variants, broker, orderer + ) + + expect(updated_lines.count).to eq 1 + expect(updated_lines[0].offer.offeredItem.semanticId) + .to eq chia_seed.semantic_links[0].semantic_id + end + end + + describe "#distributed_linked_variants" do it "selects available variants with semantic links" do variants = subject.distributed_linked_variants(order_cycle, distributor) expect(variants).to match_array [beans, chia_seed]