Build API URLs to work with any FDC Shopify shop

We can extend this service class when there are other APIs. And
hopefully the DFC will provide a standard for this discovery at some
point.
This commit is contained in:
Maikel Linke
2024-09-13 15:52:40 +10:00
parent 2eec4c73bf
commit 4303f0e974
9 changed files with 79 additions and 36 deletions

View File

@@ -1,10 +1,6 @@
# frozen_string_literal: true
class BackorderJob < ApplicationJob
FDC_BASE_URL = "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod"
FDC_CATALOG_URL = "#{FDC_BASE_URL}/SuppliedProducts".freeze
FDC_ORDERS_URL = "#{FDC_BASE_URL}/Orders".freeze
# In the current FDC project, one shop wants to review and adjust orders
# before finalising. They also run a market stall and need to adjust stock
# levels after the market. This should be done within four hours.
@@ -36,9 +32,14 @@ class BackorderJob < ApplicationJob
def self.place_backorder(order, linked_variants)
user = order.distributor.owner
orderer = FdcBackorderer.new(user)
# We are assuming that all variants are linked to the same wholesale
# shop and its catalog:
urls = FdcUrlBuilder.new(linked_variants[0].semantic_links[0].semantic_id)
orderer = FdcBackorderer.new(user, urls)
backorder = orderer.find_or_build_order(order)
broker = load_broker(order.distributor.owner)
broker = load_broker(order.distributor.owner, urls)
ordered_quantities = {}
linked_variants.each do |variant|
@@ -73,13 +74,13 @@ class BackorderJob < ApplicationJob
retail_quantity
end
def self.load_broker(user)
FdcOfferBroker.new(load_catalog(user))
def self.load_broker(user, urls)
FdcOfferBroker.new(load_catalog(user, urls))
end
def self.load_catalog(user)
def self.load_catalog(user, urls)
api = DfcRequest.new(user)
catalog_json = api.call(FDC_CATALOG_URL)
catalog_json = api.call(urls.catalog_url)
DfcIo.import(catalog_json)
end

View File

@@ -16,13 +16,13 @@ class CompleteBackorderJob < ApplicationJob
# 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)
order = FdcBackorderer.new(user, nil).find_order(order_id)
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
variants = order_cycle.variants_distributed_by(distributor)
adjust_quantities(user, order, variants)
adjust_quantities(user, order, urls, variants)
service.complete_order(order)
FdcBackorderer.new(user, urls).complete_order(order)
end
# Check if we have enough stock to reduce the backorder.
@@ -30,8 +30,8 @@ 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(user, order, variants)
broker = FdcOfferBroker.new(BackorderJob.load_catalog(user))
def adjust_quantities(user, order, urls, variants)
broker = FdcOfferBroker.new(BackorderJob.load_catalog(user, urls))
order.lines.each do |line|
line.quantity = line.quantity.to_i

View File

@@ -2,14 +2,11 @@
# Place and update orders based on missing stock.
class FdcBackorderer
FDC_BASE_URL = "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod"
FDC_ORDERS_URL = "#{FDC_BASE_URL}/Orders".freeze
FDC_SALE_SESSION_URL = "#{FDC_BASE_URL}/SalesSession/#".freeze
attr_reader :user, :urls
attr_reader :user
def initialize(user)
def initialize(user, urls)
@user = user
@urls = urls
end
def find_or_build_order(ofn_order)
@@ -17,13 +14,13 @@ class FdcBackorderer
end
def build_new_order(ofn_order)
OrderBuilder.new_order(ofn_order, FDC_ORDERS_URL).tap do |order|
OrderBuilder.new_order(ofn_order, urls.orders_url).tap do |order|
order.saleSession = build_sale_session(ofn_order)
end
end
def find_open_order
graph = import(FDC_ORDERS_URL)
graph = import(urls.orders_url)
open_orders = graph&.select do |o|
o.semanticType == "dfc-b:Order" && o.orderStatus[:path] == "Held"
end
@@ -125,12 +122,12 @@ class FdcBackorderer
end
def new?(order)
order.semanticId == FDC_ORDERS_URL
order.semanticId == urls.orders_url
end
def build_sale_session(order)
SaleSessionBuilder.build(order.order_cycle).tap do |session|
session.semanticId = FDC_SALE_SESSION_URL
session.semanticId = urls.sale_session_url
end
end
end

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
# The DFC standard doesn't include endpoint discovery yet.
# So for now we are guessing URLs based on our FDC pilot project.
class FdcUrlBuilder
attr_reader :catalog_url, :orders_url, :sale_session_url
# At the moment, we start with a product link like this:
#
# https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635
def initialize(semantic_id)
@catalog_url, _slash, _id = semantic_id.rpartition("/")
@orders_url = @catalog_url.sub("/SuppliedProducts", "/Orders")
@sale_session_url = @catalog_url.sub("/SuppliedProducts", "/SalesSession/#")
end
end

View File

@@ -57,7 +57,8 @@ RSpec.describe BackorderJob do
)
completion_time = Date.tomorrow.noon + 4.hours
orderer = FdcBackorderer.new(user)
urls = FdcUrlBuilder.new(product_link)
orderer = FdcBackorderer.new(user, urls)
backorder = orderer.build_new_order(order)
backorder.client = "https://openfoodnetwork.org.uk/api/dfc/enterprises/203468"

View File

@@ -4,7 +4,11 @@ require 'spec_helper'
RSpec.describe CompleteBackorderJob do
let(:user) { build(:testdfc_user) }
let(:catalog) { BackorderJob.load_catalog(user) }
let(:catalog) { BackorderJob.load_catalog(user, urls) }
let(:urls) { FdcUrlBuilder.new(product_link) }
let(:product_link) {
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635"
}
let(:retail_product) {
catalog.find { |item| item.semanticType == "dfc-b:SuppliedProduct" }
}
@@ -12,7 +16,7 @@ RSpec.describe CompleteBackorderJob do
flow = catalog.find { |item| item.semanticType == "dfc-b:AsPlannedProductionFlow" }
catalog.find { |item| item.semanticId == flow.product }
}
let(:orderer) { FdcBackorderer.new(user) }
let(:orderer) { FdcBackorderer.new(user, urls) }
let(:order) {
backorder = orderer.find_or_build_order(ofn_order)
broker = FdcOfferBroker.new(catalog)

View File

@@ -3,7 +3,11 @@
require 'spec_helper'
RSpec.describe FdcBackorderer do
let(:subject) { FdcBackorderer.new(order.distributor.owner) }
let(:subject) { FdcBackorderer.new(order.distributor.owner, urls) }
let(:urls) { FdcUrlBuilder.new(product_link) }
let(:product_link) {
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635"
}
let(:order) { create(:completed_order_with_totals) }
let(:account) {
OidcAccount.new(
@@ -25,11 +29,11 @@ RSpec.describe FdcBackorderer do
# Build a new order when no open one is found:
order.order_cycle = build(:order_cycle)
backorder = subject.find_or_build_order(order)
expect(backorder.semanticId).to eq FdcBackorderer::FDC_ORDERS_URL
expect(backorder.semanticId).to eq urls.orders_url
expect(backorder.lines).to eq []
# Add items and place the new order:
catalog = BackorderJob.load_catalog(order.distributor.owner)
catalog = BackorderJob.load_catalog(order.distributor.owner, urls)
product = catalog.find { |i| i.semanticType == "dfc-b:SuppliedProduct" }
offer = FdcOfferBroker.new(nil).offer_of(product)
line = subject.find_or_build_order_line(backorder, offer)
@@ -55,19 +59,19 @@ RSpec.describe FdcBackorderer do
describe "#find_or_build_order" do
it "builds an order object" do
account.updated_at = Time.zone.now
stub_request(:get, FdcBackorderer::FDC_ORDERS_URL)
stub_request(:get, urls.orders_url)
.to_return(status: 200, body: "{}")
backorder = subject.find_or_build_order(order)
expect(backorder.semanticId).to eq FdcBackorderer::FDC_ORDERS_URL
expect(backorder.semanticId).to eq urls.orders_url
expect(backorder.lines).to eq []
end
end
describe "#find_or_build_order_line" do
it "add quantity to an existing line item", vcr: true do
catalog = BackorderJob.load_catalog(order.distributor.owner)
catalog = BackorderJob.load_catalog(order.distributor.owner, urls)
backorder = subject.find_or_build_order(order)
existing_line = backorder.lines[0]

View File

@@ -4,7 +4,11 @@ require 'spec_helper'
RSpec.describe FdcOfferBroker do
subject { FdcOfferBroker.new(catalog) }
let(:catalog) { BackorderJob.load_catalog(user) }
let(:catalog) { BackorderJob.load_catalog(user, urls) }
let(:urls) { FdcUrlBuilder.new(product_link) }
let(:product_link) {
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635"
}
let(:user) { build(:testdfc_user) }
let(:product) {
catalog.find { |item| item.semanticType == "dfc-b:SuppliedProduct" }

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe FdcUrlBuilder do
subject(:urls) { FdcUrlBuilder.new(product_link) }
let(:product_link) {
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635"
}
it "knows the right URLs" do
expect(subject.catalog_url).to eq "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts"
expect(subject.orders_url).to eq "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/Orders"
expect(subject.sale_session_url).to eq "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SalesSession/#"
end
end