-
-
Notifications
You must be signed in to change notification settings - Fork 729
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
[DFC Orders] Sync remote products when order cycle opens #13167
Open
dacook
wants to merge
15
commits into
openfoodfoundation:master
Choose a base branch
from
dacook:sync-products-on-oc-opened-12986
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
6aa2c14
Synchronise remote products when order cycle opens
dacook a5b98ca
Mark each order cycle as opened once pre-conditions have been processed
dacook 9b9612d
Refactor spec
dacook 3723781
Record the exact event time for the webhook
dacook 7ea5b30
Move to new job
dacook 2150e53
Change concurrency check to be based on order_cycle.opened_at
dacook 9079fbd
Explicitly set timecop freeze time
dacook 65d8c56
Reduce specificy of test
dacook 8b11b2e
Use existing method
dacook f0fbb57
Only update one timestamp
dacook 980a24b
Use correct testing methods
dacook 2f4b81c
Re-record fixture
dacook 9220f71
Raise error if record not found, and don't retry too many times
dacook e6a7a87
Rename OrderCycleOpenedJob
dacook 6d5aaa1
Clean up unused include
dacook File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# frozen_string_literal: true | ||
|
||
# Run any pre-conditions and mark order cycle as open. | ||
# | ||
# Currently, an order cycle is considered open in the shopfront when orders_open_at >= now. | ||
# But now there are some pre-conditions for opening an order cycle, so we would like to change that. | ||
# Instead, the presence of opened_at (and absence of processed_at) should indicate it is open. | ||
class OpenOrderCycleJob < ApplicationJob | ||
sidekiq_options retry_for: 10.minutes | ||
|
||
def perform(order_cycle_id) | ||
ActiveRecord::Base.transaction do | ||
# Fetch order cycle if it's still unopened, and lock DB row until finished | ||
order_cycle = OrderCycle.lock.find_by!(id: order_cycle_id, opened_at: nil) | ||
|
||
sync_remote_variants(order_cycle) | ||
|
||
# Mark as opened | ||
opened_at = Time.zone.now | ||
order_cycle.update_columns(opened_at:) | ||
|
||
# And notify any subscribers | ||
OrderCycles::WebhookService.create_webhook_job(order_cycle, 'order_cycle.opened', opened_at) | ||
end | ||
end | ||
|
||
private | ||
|
||
def sync_remote_variants(order_cycle) | ||
# Sync any remote variants for each supplier | ||
order_cycle.suppliers.each do |supplier| | ||
links = variant_links_for(order_cycle, supplier) | ||
next if links.empty? | ||
|
||
# Find authorised user to access remote products | ||
dfc_user = supplier.owner # we assume the owner's account is the one used to import from dfc. | ||
|
||
import_variants(links, dfc_user) | ||
end | ||
end | ||
|
||
# Fetch all remote variants for this supplier in the order cycle | ||
def variant_links_for(order_cycle, supplier) | ||
variants = order_cycle.exchanges.incoming.from_enterprise(supplier) | ||
.joins(:exchange_variants).select('exchange_variants.variant_id') | ||
SemanticLink.where(subject_id: variants) | ||
end | ||
|
||
def import_variants(links, dfc_user) | ||
# Find any catalogues associated with the variants | ||
catalogs = links.group_by do |link| | ||
FdcUrlBuilder.new(link.semantic_id).catalog_url | ||
end | ||
|
||
# Import selected variants from each catalog | ||
catalogs.each do |catalog_url, catalog_links| | ||
catalog = DfcCatalog.load(dfc_user, catalog_url) | ||
catalog.apply_wholesale_values! | ||
|
||
catalog_links.each do |link| | ||
catalog_item = catalog.item(link.semantic_id) | ||
|
||
SuppliedProductImporter.update_product(catalog_item, link.subject) if catalog_item | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# frozen_string_literal: true | ||
|
||
# Trigger jobs for any order cycles that recently opened | ||
class TriggerOrderCyclesToOpenJob < ApplicationJob | ||
def perform | ||
recently_opened_order_cycles.find_each do |order_cycle| | ||
OpenOrderCycleJob.perform_later(order_cycle.id) | ||
end | ||
end | ||
|
||
private | ||
|
||
def recently_opened_order_cycles | ||
OrderCycle | ||
.where(opened_at: nil) | ||
.where(orders_open_at: 1.hour.ago..Time.zone.now) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
uid { user&.email || generate(:random_email) } | ||
|
||
# This is a live test account authenticated via Les Communes. | ||
# See .env.test for tips on connecting the account for recording VCR cassettes. | ||
factory :testdfc_account do | ||
uid { "[email protected]" } | ||
refresh_token { ENV.fetch("OPENID_REFRESH_TOKEN") } | ||
|
198 changes: 198 additions & 0 deletions
198
...es/OpenOrderCycleJob/syncing_remote_products/synchronises_products_from_a_FDC_catalog.yml
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'spec_helper' | ||
|
||
RSpec.describe OpenOrderCycleJob do | ||
let(:now){ Time.zone.now } | ||
let(:order_cycle) { create(:simple_order_cycle, orders_open_at: now) } | ||
subject { OpenOrderCycleJob.perform_now(order_cycle.id) } | ||
|
||
around do |example| | ||
Timecop.freeze(now) { example.run } | ||
end | ||
|
||
it "marks as open" do | ||
expect { | ||
subject | ||
order_cycle.reload | ||
} | ||
.to change { order_cycle.opened_at } | ||
|
||
expect(order_cycle.opened_at).to be_within(1).of(now) | ||
end | ||
|
||
it "enqueues webhook job" do | ||
expect(OrderCycles::WebhookService) | ||
.to receive(:create_webhook_job).with(order_cycle, 'order_cycle.opened', now).once | ||
|
||
subject | ||
end | ||
|
||
describe "syncing remote products" do | ||
let!(:user) { create(:testdfc_user, owned_enterprises: [enterprise]) } | ||
|
||
let(:enterprise) { create(:supplier_enterprise) } | ||
let!(:variant) { create(:variant, name: "Sauce", supplier_id: enterprise.id) } | ||
let!(:order_cycle) { | ||
create(:simple_order_cycle, orders_open_at: now, | ||
suppliers: [enterprise], variants: [variant]) | ||
} | ||
|
||
it "synchronises products from a FDC catalog", vcr: true do | ||
user.update!(oidc_account: build(:testdfc_account)) | ||
# One product is existing in OFN | ||
product_id = | ||
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635" | ||
variant.semantic_links << SemanticLink.new(semantic_id: product_id) | ||
|
||
expect { | ||
subject | ||
variant.reload | ||
order_cycle.reload | ||
}.to change { order_cycle.opened_at } | ||
.and change { enterprise.supplied_products.count }.by(0) # It shouldn't add, only update | ||
.and change { variant.display_name } | ||
.and change { variant.unit_value } | ||
# 18.85 wholesale variant price divided by 12 cans in the slab. | ||
.and change { variant.price }.to(1.57) | ||
.and change { variant.on_demand }.to(true) | ||
.and change { variant.on_hand }.by(0) | ||
.and query_database 46 | ||
end | ||
end | ||
|
||
describe "concurrency", concurrency: true do | ||
let(:breakpoint) { Mutex.new } | ||
|
||
it "doesn't open order cycle twice" do | ||
# Pause jobs when placing new job: | ||
breakpoint.lock | ||
allow(OpenOrderCycleJob).to( | ||
receive(:new).and_wrap_original do |method, *args| | ||
breakpoint.synchronize {} # rubocop:disable Lint/EmptyBlock | ||
method.call(*args) | ||
end | ||
) | ||
|
||
expect(OrderCycles::WebhookService) | ||
.to receive(:create_webhook_job).with(order_cycle, 'order_cycle.opened', now).once | ||
|
||
expect{ | ||
# Start two jobs in parallel: | ||
threads = [ | ||
Thread.new { OpenOrderCycleJob.perform_now(order_cycle.id) }, | ||
Thread.new { OpenOrderCycleJob.perform_now(order_cycle.id) }, | ||
] | ||
|
||
# Wait for both to jobs to pause. | ||
# This can reveal a race condition. | ||
sleep 0.1 | ||
|
||
# Resume and complete both jobs: | ||
breakpoint.unlock | ||
threads.each(&:join) | ||
}.to raise_error ActiveRecord::RecordNotFound | ||
end | ||
end | ||
end |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'spec_helper' | ||
|
||
RSpec.describe TriggerOrderCyclesToOpenJob do | ||
let(:oc_opened_before) { | ||
create(:simple_order_cycle, orders_open_at: 1.hour.ago) | ||
} | ||
let(:oc_opened_now) { | ||
create(:simple_order_cycle, orders_open_at: Time.zone.now) | ||
} | ||
let(:oc_opening_soon) { | ||
create(:simple_order_cycle, orders_open_at: 1.minute.from_now) | ||
} | ||
|
||
it "enqueues jobs for recently opened order cycles only" do | ||
expect{ TriggerOrderCyclesToOpenJob.perform_now } | ||
.to enqueue_job(OpenOrderCycleJob).with(oc_opened_now.id) | ||
.and enqueue_job(OpenOrderCycleJob).with(oc_opened_before.id).exactly(0).times | ||
.and enqueue_job(OpenOrderCycleJob).with(oc_opening_soon.id).exactly(0).times | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I thought that there was some way to convert the timestamp to the db precision. I can't remember it now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this was just the first solution I came across, and it worked :)
Another thought was to use
to_i
when comparing.