Adjust quantities of backorder before completion

This commit is contained in:
Maikel Linke
2024-09-12 15:53:22 +10:00
parent 95e620a78b
commit 283db8f9d0
7 changed files with 349 additions and 160 deletions

View File

@@ -87,7 +87,9 @@ class BackorderJob < ApplicationJob
wait_until = order.order_cycle.orders_close_at + SALE_SESSION_DELAY
CompleteBackorderJob.set(wait_until:)
.perform_later(user, placed_order.semanticId)
.perform_later(
user, order.distributor, order.order_cycle, placed_order.semanticId
)
end
def perform(*args)

View File

@@ -3,12 +3,25 @@
# After an order cycle closed, we need to finalise open draft orders placed
# to replenish stock.
class CompleteBackorderJob < ApplicationJob
def perform(user, order_id)
# TODO: review our stock levels and adjust quantities if we got surplus.
# This can happen when orders are cancelled and products restocked.
# Required parameters:
#
# * user: to authenticate DFC requests
# * distributor: to reconile with its catalog
# * order_cycle: to scope the catalog when looking up variants
# Multiple variants can be linked to the same remote product.
# To reduce ambiguity, we'll reconcile only with products
# from the given distributor in a given order cycle for which
# the remote backorder was placed.
# * order_id: the remote semantic id of a draft order
# Having the id makes sure that we don't accidentally finalise
# someone else's order.
def perform(user, distributor, order_cycle, order_id)
service = FdcBackorderer.new(user)
order = service.find_order(order_id)
adjust_quantities(order)
variants = order_cycle.variants_distributed_by(distributor)
adjust_quantities(user, order, variants)
service.complete_order(order)
end
@@ -17,7 +30,20 @@ class CompleteBackorderJob < ApplicationJob
# Our local stock can increase when users cancel their orders.
# But stock levels could also have been adjusted manually. So we review all
# quantities before finalising the order.
def adjust_quantities(order)
# TODO
def adjust_quantities(user, order, variants)
broker = FdcOfferBroker.new(BackorderJob.load_catalog(user))
order.lines.each do |line|
wholesale_product_id = line.offer.offeredItem.semanticId
transformation = broker.wholesale_to_retail(wholesale_product_id)
linked_variant = variants.linked_to(transformation.retail_product_id)
# Note that a division of integers dismisses the remainder, like `floor`:
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
line.quantity = line.quantity.to_i - wholesale_items_contained_in_stock
retail_stock_changes = wholesale_items_contained_in_stock * transformation.factor
linked_variant.on_hand -= retail_stock_changes
end
end
end

View File

@@ -2,7 +2,9 @@
# Finds wholesale offers for retail products.
class FdcOfferBroker
# TODO: Find a better way to provide this data.
Solution = Struct.new(:product, :factor, :offer)
RetailSolution = Struct.new(:retail_product_id, :factor)
def initialize(catalog)
@catalog = catalog
@@ -14,13 +16,25 @@ class FdcOfferBroker
contained_quantity = consumption_flow.quantity.value.to_i
wholesale_product_id = production_flow.product
wholesale_product = catalog_item(wholesale_product_id )
wholesale_product = catalog_item(wholesale_product_id)
offer = offer_of(wholesale_product)
Solution.new(wholesale_product, contained_quantity, offer)
end
def wholesale_to_retail(wholesale_product_id)
production_flow = flow_producing(wholesale_product_id)
consumption_flow = catalog_item(
production_flow.semanticId.sub("AsPlannedProductionFlow", "AsPlannedConsumptionFlow")
)
retail_product_id = consumption_flow.product
contained_quantity = consumption_flow.quantity.value.to_i
RetailSolution.new(retail_product_id, contained_quantity)
end
def offer_of(product)
product&.catalogItems&.first&.offers&.first&.tap do |offer|
# Unfortunately, the imported catalog doesn't provide the reverse link:
@@ -32,4 +46,15 @@ class FdcOfferBroker
@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(&:product)
@production_flows_by_product_id[wholesale_product_id]
end
def production_flows
@production_flows ||= @catalog.select do |i|
i.semanticType == "dfc-b:AsPlannedProductionFlow"
end
end
end

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -24,7 +24,11 @@ RSpec.describe BackorderJob do
end
it "places an order", vcr: true do
order.order_cycle = build(:order_cycle)
order.order_cycle = create(
:simple_order_cycle,
distributors: [order.distributor],
variants: [variant],
)
variant.on_demand = true
variant.on_hand = -3
variant.semantic_links << SemanticLink.new(

View File

@@ -5,26 +5,60 @@ require 'spec_helper'
RSpec.describe CompleteBackorderJob do
let(:user) { build(:testdfc_user) }
let(:catalog) { BackorderJob.load_catalog(user) }
let(:product) {
let(:retail_product) {
catalog.find { |item| item.semanticType == "dfc-b:SuppliedProduct" }
}
let(:wholesale_product) {
flow = catalog.find { |item| item.semanticType == "dfc-b:AsPlannedProductionFlow" }
catalog.find { |item| item.semanticId == flow.product }
}
let(:orderer) { FdcBackorderer.new(user) }
let(:order) {
ofn_order = build(:order, distributor_id: 1)
ofn_order.order_cycle = build(:order_cycle)
backorder = orderer.find_or_build_order(ofn_order)
offer = FdcOfferBroker.new(nil).offer_of(product)
broker = FdcOfferBroker.new(catalog)
offer = broker.best_offer(retail_product.semanticId).offer
line = orderer.find_or_build_order_line(backorder, offer)
line.quantity = 3
orderer.send_order(backorder)
}
let(:ofn_order) { create(:completed_order_with_totals) }
let(:distributor) { ofn_order.distributor }
let(:order_cycle) { ofn_order.order_cycle }
let(:variant) { ofn_order.variants[0] }
describe "#perform" do
before do
variant.semantic_links << SemanticLink.new(
semantic_id: retail_product.semanticId
)
# We are assuming 12 cans in a slab.
# We got more stock than we need.
variant.on_hand = 13
ofn_order.order_cycle = create(
:simple_order_cycle,
distributors: [distributor],
variants: [variant],
)
end
it "completes an order", vcr: true do
subject.perform(user, order.semanticId)
updated_order = orderer.find_order(order.semanticId)
expect(updated_order.orderStatus[:path]).to eq "Complete"
current_order = order
expect {
subject.perform(user, distributor, order_cycle, order.semanticId)
current_order = orderer.find_order(order.semanticId)
}.to change {
current_order.orderStatus[:path]
}.from("Held").to("Complete")
.and change {
current_order.lines[0].quantity.to_i
}.from(3).to(2)
.and change {
variant.on_hand
}.from(13).to(1)
end
end
end