Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Place backorders for linked products via DFC integration #12856

Merged
merged 37 commits into from
Oct 1, 2024
Merged
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
260e4f7
Create BackorderJob to place wholesale orders
mkllnk Mar 20, 2024
98966f6
Place backorders for DFC products
mkllnk Mar 26, 2024
439f0ca
Raise errors on DFC requests
mkllnk Aug 23, 2024
6c6927a
Add SaleSession with correct OrderCycle times
mkllnk Aug 23, 2024
827e37c
Start moving backorder logic to service
mkllnk Aug 23, 2024
caa6d28
Find and update existing open order
mkllnk Aug 30, 2024
a7a3889
Add needed quantities to existing line items
mkllnk Sep 4, 2024
f839452
Complete an open order
mkllnk Sep 6, 2024
c7fa3ff
Simplify order update logic
mkllnk Sep 6, 2024
3e0eb87
Simplify service with ivar
mkllnk Sep 6, 2024
7b286ea
Complete test for FDC Orders API
mkllnk Sep 6, 2024
3849db7
Simplify order update call
mkllnk Sep 6, 2024
3ec53a7
Parse updated order result
mkllnk Sep 6, 2024
c0ae2ed
Complete order 4 hours after order cycle closed
mkllnk Sep 6, 2024
8f4f873
Move offer finding into separate class
mkllnk Sep 10, 2024
14c32c0
Reduce complexity
mkllnk Sep 10, 2024
efe2b72
Find wholesale offer for retail variant
mkllnk Sep 10, 2024
95bc0cc
Reduce complexity of BackorderJob
mkllnk Sep 11, 2024
c948efd
Add structure to adjust final quantities
mkllnk Sep 11, 2024
95e620a
Add lookup of variants by semantic id
mkllnk Sep 12, 2024
283db8f
Adjust quantities of backorder before completion
mkllnk Sep 12, 2024
5ef85ae
Handle backorder cancellations
mkllnk Sep 13, 2024
2eec4c7
Apply 4 hour completion delay only to one enterprise
mkllnk Sep 13, 2024
4303f0e
Build API URLs to work with any FDC Shopify shop
mkllnk Sep 13, 2024
fb96f8f
Fall back to given product w/o wholesale variant
mkllnk Sep 16, 2024
070b93c
Fall back to givin product id w/o retail variant
mkllnk Sep 17, 2024
7f62b49
Move catalog loading to where it's needed
mkllnk Sep 17, 2024
66f0802
Import DFC product images
mkllnk Sep 17, 2024
9f43244
Import on-demand stock setting in DFC import
mkllnk Sep 19, 2024
2465780
Import prices and stock levels from DFC catalog
mkllnk Sep 25, 2024
eece738
Restore concurrency spec for the checkout
mkllnk Sep 25, 2024
61fec65
Abstract OrderLocker for re-use
mkllnk Sep 25, 2024
e31e45b
Place backorders in the background
mkllnk Sep 25, 2024
49fd1dc
Report backorder errors instead of failing checkout
mkllnk Sep 25, 2024
495634b
Send error notification to owner
mkllnk Sep 25, 2024
989a6d5
Notify user of failed backorder completion
mkllnk Sep 26, 2024
51b3770
Keep failed backorder job in dead set
mkllnk Sep 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions spec/requests/checkout/concurrency_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# frozen_string_literal: true

require 'spec_helper'

# This is the first example of testing concurrency in the Open Food Network.
# If we want to do this more often, we should look at:
#
# https://github.com/forkbreak/fork_break
#
# The concurrency flag enables multiple threads to see the same database
# without isolated transactions.
RSpec.describe "Concurrent checkouts", concurrency: true do
include AuthenticationHelper
include ShopWorkflow

let(:order_cycle) { create(:order_cycle) }
let(:distributor) { order_cycle.distributors.first }
let(:order) { create(:order, order_cycle:, distributor:) }
let(:address) { create(:address) }
let(:payment_method) { create(:payment_method, distributors: [distributor]) }
let(:breakpoint) { Mutex.new }

let(:address_params) { address.attributes.except("id") }
let(:order_params) {
{
"payments_attributes" => [
{
"payment_method_id" => payment_method.id,
"amount" => order.total
}
],
"bill_address_attributes" => address_params,
"ship_address_attributes" => address_params,
}
}
let(:path) { checkout_update_path(:summary) }
let(:params) { { format: :json } }

before do
# Create a valid order ready for checkout:
create(:shipping_method, distributors: [distributor])
variant = order_cycle.variants_distributed_by(distributor).first
order.line_items << create(:line_item, variant:)

# Transition cart to confirmation state:
order.update(order_params)
order.next # => address
order.next # => delivery
order.next # => payment
order.next # => confirmation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might not hurt to expect order.state to be confirmation, if perhaps the states change in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should be using Orders::WorkflowService#next to better replicate the checkout process ?


set_order(order)
login_as(order.user)
end

it "handles two concurrent orders successfully" do
breakpoint.lock
breakpoint_reached_counter = 0

# Set a breakpoint after loading the order and before advancing the order's
# state and making payments. If two requests reach this breakpoint at the
# same time, they are in a race condition and bad things can happen.
# Examples are processing payments twice or selling more than we have.
allow_any_instance_of(CheckoutController).
to receive(:advance_order_state).
and_wrap_original do |method, *args|
breakpoint_reached_counter += 1
breakpoint.synchronize do
# Wait here until the breakpoint is unlocked.
# Hopefully only one thread gets here in that time.
# The second thread is told by the controller to wait before
# loading the order.
end
method.call(*args)
end

# Starting two checkout threads. The controller code will determine if
# these two threads are synchronised correctly or run into a race condition.
#
# 1. If the controller synchronises correctly:
# The first thread locks required resources and then waits at the
# breakpoint. The second thread waits for the first one.
# 2. If the controller fails to prevent the race condition:
# Both threads load required resources and wait at the breakpoint to do
# the same checkout action.
threads = [
Thread.new { put(path, params:) },
Thread.new { put(path, params:) },
]

# Wait for the first thread to reach the breakpoint:
Timeout.timeout(1) do
sleep 0.1 while breakpoint_reached_counter < 1
end

# Give the second thread a chance to reach the breakpoint, too.
# But we hope that it waits for the first thread earlier and doesn't
# reach the breakpoint yet.
sleep 1
expect(breakpoint_reached_counter).to eq 1

# Let the requests continue and finish.
breakpoint.unlock
threads.each(&:join)

# Verify that the checkout happened once.
order.reload
expect(order.completed?).to be true
expect(order.payments.count).to eq 1
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🏅