mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Extract subs placement job logic into new service
While doing that we pass stock changes to the service but we lazy-evaluate them. This way we don't fetch all this data from DB when it might not be used due to an early return. Also, this makes it possible to save the stock-related logic for later. Finally, when changing things to rely on `#initialize_order`'s boolean return value I noticed though, that we were evaluating `proxy_order.order` too early. When instantiating the service object it won't exist yet.
This commit is contained in:
@@ -9,14 +9,11 @@ class SubscriptionPlacementJob < ActiveJob::Base
|
||||
place_order_for(proxy_order)
|
||||
end
|
||||
|
||||
send_placement_summary_emails
|
||||
summarizer.send_placement_summary_emails
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
delegate :record_success, :record_issue, :record_subscription_issue, to: :summarizer
|
||||
delegate :record_order, :record_and_log_error, :send_placement_summary_emails, to: :summarizer
|
||||
|
||||
def summarizer
|
||||
@summarizer ||= OrderManagement::Subscriptions::Summarizer.new
|
||||
end
|
||||
@@ -29,33 +26,14 @@ class SubscriptionPlacementJob < ActiveJob::Base
|
||||
end
|
||||
|
||||
def place_order_for(proxy_order)
|
||||
JobLogger.logger.info("Placing Order for Proxy Order #{proxy_order.id}")
|
||||
initialise_order(proxy_order)
|
||||
return unless proxy_order.order.present?
|
||||
place_order = PlaceOrder.new(
|
||||
proxy_order,
|
||||
summarizer,
|
||||
JobLogger.logger,
|
||||
lambda { cap_quantity_and_store_changes(proxy_order.order) }
|
||||
)
|
||||
|
||||
proxy_order.update_column(:placed_at, Time.zone.now)
|
||||
place_order(proxy_order.order)
|
||||
end
|
||||
|
||||
def initialise_order(proxy_order)
|
||||
proxy_order.initialise_order!
|
||||
record_subscription_issue(proxy_order.subscription) if proxy_order.order.nil?
|
||||
rescue StandardError => e
|
||||
Bugsnag.notify(e, subscription: proxy_order.subscription, proxy_order: proxy_order)
|
||||
end
|
||||
|
||||
def place_order(order)
|
||||
record_order(order)
|
||||
return record_issue(:complete, order) if order.completed?
|
||||
|
||||
changes = cap_quantity_and_store_changes(order)
|
||||
return handle_empty_order(order, changes) if order.line_items.where('quantity > 0').empty?
|
||||
|
||||
move_to_completion(order)
|
||||
send_placement_email(order, changes)
|
||||
rescue StandardError => e
|
||||
record_and_log_error(:processing, order, e.message)
|
||||
Bugsnag.notify(e, order: order)
|
||||
place_order.call
|
||||
end
|
||||
|
||||
def cap_quantity_and_store_changes(order)
|
||||
@@ -77,16 +55,6 @@ class SubscriptionPlacementJob < ActiveJob::Base
|
||||
changes
|
||||
end
|
||||
|
||||
def handle_empty_order(order, changes)
|
||||
order.reload.all_adjustments.destroy_all
|
||||
order.update_order!
|
||||
send_empty_email(order, changes)
|
||||
end
|
||||
|
||||
def move_to_completion(order)
|
||||
OrderWorkflow.new(order).complete!
|
||||
end
|
||||
|
||||
def unavailable_stock_lines_for(order)
|
||||
order.line_items.where('variant_id NOT IN (?)', available_variants_for(order).select(&:id))
|
||||
end
|
||||
@@ -94,15 +62,4 @@ class SubscriptionPlacementJob < ActiveJob::Base
|
||||
def available_variants_for(order)
|
||||
OrderCycleDistributedVariants.new(order.order_cycle, order.distributor).available_variants
|
||||
end
|
||||
|
||||
def send_placement_email(order, changes)
|
||||
record_issue(:changes, order) if changes.present?
|
||||
record_success(order) if changes.blank?
|
||||
SubscriptionMailer.placement_email(order, changes).deliver_now
|
||||
end
|
||||
|
||||
def send_empty_email(order, changes)
|
||||
record_issue(:empty, order)
|
||||
SubscriptionMailer.empty_email(order, changes).deliver_now
|
||||
end
|
||||
end
|
||||
|
||||
82
app/services/place_order.rb
Normal file
82
app/services/place_order.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PlaceOrder
|
||||
def initialize(proxy_order, summarizer, logger, changes_loader)
|
||||
@proxy_order = proxy_order
|
||||
@subscription = proxy_order.subscription
|
||||
@summarizer = summarizer
|
||||
@logger = logger
|
||||
@changes_loader = changes_loader
|
||||
end
|
||||
|
||||
def call
|
||||
return unless initialise_order
|
||||
|
||||
mark_as_processed
|
||||
|
||||
summarizer.record_order(order)
|
||||
return summarizer.record_issue(:complete, order) if order.completed?
|
||||
|
||||
load_changes
|
||||
return handle_empty_order if empty_order?
|
||||
|
||||
move_to_completion
|
||||
send_placement_email
|
||||
rescue StandardError => e
|
||||
summarizer.record_and_log_error(:processing, e.message)
|
||||
Bugsnag.notify(e, order: order)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :proxy_order, :subscription, :order, :summarizer, :logger, :changes_loader, :changes
|
||||
|
||||
def initialise_order
|
||||
logger.info("Placing Order for Proxy Order #{proxy_order.id}")
|
||||
|
||||
@order = proxy_order.initialise_order!
|
||||
|
||||
if order.nil?
|
||||
summarizer.record_subscription_issue(subscription)
|
||||
return false
|
||||
end
|
||||
true
|
||||
rescue StandardError => e
|
||||
Bugsnag.notify(e, subscription: subscription, proxy_order: proxy_order)
|
||||
false
|
||||
end
|
||||
|
||||
def mark_as_processed
|
||||
proxy_order.update_column(:placed_at, Time.now)
|
||||
end
|
||||
|
||||
def load_changes
|
||||
@changes = changes_loader.call
|
||||
end
|
||||
|
||||
def empty_order?
|
||||
order.line_items.where('quantity > 0').empty?
|
||||
end
|
||||
|
||||
def handle_empty_order
|
||||
order.reload.all_adjustments.destroy_all
|
||||
order.update_order!
|
||||
send_empty_email
|
||||
end
|
||||
|
||||
def send_empty_email
|
||||
summarizer.record_issue(:empty, order)
|
||||
SubscriptionMailer.empty_email(order, changes).deliver_now
|
||||
end
|
||||
|
||||
def move_to_completion
|
||||
OrderWorkflow.new(order).complete!
|
||||
end
|
||||
|
||||
def send_placement_email
|
||||
summarizer.record_issue(:changes, order) if changes.present?
|
||||
summarizer.record_success(order) if changes.blank?
|
||||
|
||||
SubscriptionMailer.placement_email(order, changes).deliver_now
|
||||
end
|
||||
end
|
||||
@@ -51,21 +51,25 @@ describe SubscriptionPlacementJob do
|
||||
describe "performing the job" do
|
||||
context "when unplaced proxy_orders exist" do
|
||||
let!(:subscription) { create(:subscription, with_items: true) }
|
||||
let!(:proxy_order) { create(:proxy_order, subscription: subscription) }
|
||||
let!(:proxy_order) { create(:proxy_order, subscription: subscription, order: build(:order)) }
|
||||
|
||||
before do
|
||||
allow(job).to receive(:proxy_orders) { ProxyOrder.where(id: proxy_order.id) }
|
||||
allow(job).to receive(:place_order)
|
||||
end
|
||||
|
||||
it "marks placeable proxy_orders as processed by setting placed_at" do
|
||||
expect{ job.perform }.to change{ proxy_order.reload.placed_at }
|
||||
expect(proxy_order.placed_at).to be_within(5.seconds).of Time.zone.now
|
||||
end
|
||||
|
||||
it "processes placeable proxy_orders" do
|
||||
summarizer = instance_double(OrderManagement::Subscriptions::Summarizer)
|
||||
service = PlaceOrder.new(
|
||||
proxy_order,
|
||||
summarizer,
|
||||
JobLogger.logger,
|
||||
lambda { job.send(:cap_quantity_and_store_changes, order) }
|
||||
)
|
||||
|
||||
allow(PlaceOrder).to receive(:new) { service }
|
||||
allow(service).to receive(:call)
|
||||
job.perform
|
||||
expect(job).to have_received(:place_order).with(proxy_order.reload.order)
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -125,18 +129,6 @@ describe SubscriptionPlacementJob do
|
||||
expect(changes[line_item2.id]).to be 3
|
||||
expect(changes[line_item3.id]).to be 3
|
||||
end
|
||||
|
||||
context "and the order has been placed" do
|
||||
before do
|
||||
allow(order).to receive(:ensure_available_shipping_rates) { true }
|
||||
allow(order).to receive(:process_each_payment) { true }
|
||||
job.send(:place_order, order.reload)
|
||||
end
|
||||
|
||||
it "removes the unavailable items from the shipment" do
|
||||
expect(order.shipment.manifest.size).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -157,17 +149,25 @@ describe SubscriptionPlacementJob do
|
||||
|
||||
before do
|
||||
expect_any_instance_of(Spree::Payment).to_not receive(:process!)
|
||||
allow(job).to receive(:send_placement_email)
|
||||
allow(job).to receive(:send_empty_email)
|
||||
allow_any_instance_of(PlaceOrder).to receive(:send_placement_email)
|
||||
allow_any_instance_of(PlaceOrder).to receive(:send_empty_email)
|
||||
end
|
||||
|
||||
context "when the order is already complete" do
|
||||
before { break unless order.next! while !order.completed? }
|
||||
|
||||
it "records an issue and ignores it" do
|
||||
summarizer = instance_double(OrderManagement::Subscriptions::Summarizer, record_order: true)
|
||||
service = PlaceOrder.new(
|
||||
proxy_order,
|
||||
summarizer,
|
||||
JobLogger.logger,
|
||||
lambda { job.send(:cap_quantity_and_store_changes, order) }
|
||||
)
|
||||
|
||||
ActionMailer::Base.deliveries.clear
|
||||
expect(job).to receive(:record_issue).with(:complete, order).once
|
||||
expect{ job.send(:place_order, order) }.to_not change{ order.reload.state }
|
||||
expect(summarizer).to receive(:record_issue).with(:complete, order).once
|
||||
expect{ service.call }.to_not change{ order.reload.state }
|
||||
expect(order.payments.first.state).to eq "checkout"
|
||||
expect(ActionMailer::Base.deliveries.count).to be 0
|
||||
end
|
||||
@@ -180,9 +180,10 @@ describe SubscriptionPlacementJob do
|
||||
end
|
||||
|
||||
it "uses the same shipping method after advancing the order" do
|
||||
job.send(:place_order, order)
|
||||
expect(order.state).to eq "complete"
|
||||
allow(proxy_order).to receive(:order) { order }
|
||||
job.send(:place_order_for, proxy_order)
|
||||
order.reload
|
||||
expect(order.state).to eq "complete"
|
||||
expect(order.shipping_method).to eq(shipping_method)
|
||||
end
|
||||
end
|
||||
@@ -193,40 +194,65 @@ describe SubscriptionPlacementJob do
|
||||
end
|
||||
|
||||
it "does not place the order, clears all adjustments, and sends an empty_order email" do
|
||||
expect{ job.send(:place_order, order) }.to_not change{
|
||||
order.reload.completed_at
|
||||
}.from(nil)
|
||||
summarizer = instance_double(OrderManagement::Subscriptions::Summarizer, record_order: true, record_issue: true)
|
||||
service = PlaceOrder.new(
|
||||
proxy_order,
|
||||
summarizer,
|
||||
JobLogger.logger,
|
||||
lambda { job.send(:cap_quantity_and_store_changes, order) }
|
||||
)
|
||||
|
||||
allow(service).to receive(:send_placement_email)
|
||||
allow(service).to receive(:send_empty_email)
|
||||
|
||||
expect{ service.call }.to_not change{ order.reload.completed_at }.from(nil)
|
||||
expect(order.all_adjustments).to be_empty
|
||||
expect(order.total).to eq 0
|
||||
expect(order.adjustment_total).to eq 0
|
||||
expect(job).to_not have_received(:send_placement_email)
|
||||
expect(job).to have_received(:send_empty_email)
|
||||
expect(service).to_not have_received(:send_placement_email)
|
||||
expect(service).to have_received(:send_empty_email)
|
||||
end
|
||||
end
|
||||
|
||||
context "when at least one stock item is available after capping stock" do
|
||||
let(:summarizer) do
|
||||
instance_double(OrderManagement::Subscriptions::Summarizer, record_order: true, record_success: true)
|
||||
end
|
||||
let(:service) do
|
||||
PlaceOrder.new(
|
||||
proxy_order,
|
||||
summarizer,
|
||||
JobLogger.logger,
|
||||
lambda { job.send(:cap_quantity_and_store_changes, order) }
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(service).to receive(:send_placement_email)
|
||||
end
|
||||
|
||||
it "processes the order to completion, but does not process the payment" do
|
||||
# If this spec starts complaining about no shipping methods being available
|
||||
# on CI, there is probably another spec resetting the currency though Rails.cache.clear
|
||||
expect{ job.send(:place_order, order) }.to change{ order.reload.completed_at }.from(nil)
|
||||
expect{ service.call }.to change{ order.reload.completed_at }.from(nil)
|
||||
expect(order.completed_at).to be_within(5.seconds).of Time.zone.now
|
||||
expect(order.payments.first.state).to eq "checkout"
|
||||
end
|
||||
|
||||
it "does not enqueue confirmation emails" do
|
||||
expect{ job.send(:place_order, order) }
|
||||
expect{ service.call }
|
||||
.to_not have_enqueued_mail(Spree::OrderMailer, :confirm_email_for_customer)
|
||||
|
||||
expect(job).to have_received(:send_placement_email).with(order, anything).once
|
||||
expect(service).to have_received(:send_placement_email).once
|
||||
end
|
||||
|
||||
context "when progression of the order fails" do
|
||||
before { allow(order).to receive(:next) { false } }
|
||||
before { allow(service).to receive(:move_to_completion).and_raise(StandardError) }
|
||||
|
||||
it "records an error and does not attempt to send an email" do
|
||||
expect(job).to_not receive(:send_placement_email)
|
||||
expect(job).to receive(:record_and_log_error).once
|
||||
job.send(:place_order, order)
|
||||
expect(service).to_not receive(:send_placement_email)
|
||||
expect(summarizer).to receive(:record_and_log_error).once
|
||||
service.call
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -238,58 +264,10 @@ describe SubscriptionPlacementJob do
|
||||
end
|
||||
|
||||
it "records an error " do
|
||||
expect(job).to receive(:record_subscription_issue)
|
||||
expect_any_instance_of(OrderManagement::Subscriptions::Summarizer).to receive(:record_subscription_issue)
|
||||
expect(job).to_not receive(:place_order)
|
||||
job.send(:place_order_for, proxy_order)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_placement_email" do
|
||||
let!(:order) { double(:order) }
|
||||
let(:mail_mock) { double(:mailer_mock, deliver_now: true) }
|
||||
|
||||
before do
|
||||
allow(SubscriptionMailer).to receive(:placement_email) { mail_mock }
|
||||
end
|
||||
|
||||
context "when changes are present" do
|
||||
let(:changes) { double(:changes) }
|
||||
|
||||
it "logs an issue and sends the email" do
|
||||
expect(job).to receive(:record_issue).with(:changes, order).once
|
||||
job.send(:send_placement_email, order, changes)
|
||||
expect(SubscriptionMailer).to have_received(:placement_email).with(order, changes)
|
||||
expect(mail_mock).to have_received(:deliver_now)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no changes are present" do
|
||||
let(:changes) { {} }
|
||||
|
||||
it "logs a success and sends the email" do
|
||||
expect(job).to receive(:record_success).with(order).once
|
||||
job.send(:send_placement_email, order, changes)
|
||||
expect(SubscriptionMailer).to have_received(:placement_email)
|
||||
expect(mail_mock).to have_received(:deliver_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_empty_email" do
|
||||
let!(:order) { double(:order) }
|
||||
let(:changes) { double(:changes) }
|
||||
let(:mail_mock) { double(:mailer_mock, deliver_now: true) }
|
||||
|
||||
before do
|
||||
allow(SubscriptionMailer).to receive(:empty_email) { mail_mock }
|
||||
end
|
||||
|
||||
it "logs an issue and sends the email" do
|
||||
expect(job).to receive(:record_issue).with(:empty, order).once
|
||||
job.send(:send_empty_email, order, changes)
|
||||
expect(SubscriptionMailer).to have_received(:empty_email).with(order, changes)
|
||||
expect(mail_mock).to have_received(:deliver_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
132
spec/services/place_order_spec.rb
Normal file
132
spec/services/place_order_spec.rb
Normal file
@@ -0,0 +1,132 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe PlaceOrder do
|
||||
subject { described_class.new(proxy_order, summarizer, logger, changes) }
|
||||
|
||||
let(:changes) { {} }
|
||||
let(:proxy_order) { create(:proxy_order, order: order) }
|
||||
let(:order) { build(:order) }
|
||||
let(:summarizer) { instance_double(OrderManagement::Subscriptions::Summarizer) }
|
||||
let(:logger) { instance_double(JobLogger.logger.class, info: true) }
|
||||
|
||||
let(:mail_mock) { double(:mailer_mock, deliver_now: true) }
|
||||
|
||||
describe "#call" do
|
||||
let!(:subscription) { create(:subscription, with_items: true) }
|
||||
let!(:proxy_order) { create(:proxy_order, subscription: subscription, order: order) }
|
||||
|
||||
let(:changes) { lambda { {} } }
|
||||
let(:summarizer) { instance_double(OrderManagement::Subscriptions::Summarizer, record_order: true, record_issue: true) }
|
||||
|
||||
before do
|
||||
allow(SubscriptionMailer).to receive(:empty_email) { mail_mock }
|
||||
subject.initialise_order
|
||||
end
|
||||
|
||||
it "marks placeable proxy_orders as processed by setting placed_at" do
|
||||
expect{ subject.call(order, subject) }.to change{ proxy_order.reload.placed_at }
|
||||
expect(proxy_order.placed_at).to be_within(5.seconds).of Time.zone.now
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_placement_email" do
|
||||
let(:summarizer) { instance_double(OrderManagement::Subscriptions::Summarizer, record_order: true) }
|
||||
|
||||
before do
|
||||
allow(SubscriptionMailer).to receive(:placement_email) { mail_mock }
|
||||
end
|
||||
|
||||
before do
|
||||
order.line_items << build(:line_item)
|
||||
|
||||
order_workflow = instance_double(OrderWorkflow, complete!: true)
|
||||
allow(OrderWorkflow).to receive(:new).with(order).and_return(order_workflow)
|
||||
end
|
||||
|
||||
context "when no changes are present" do
|
||||
let(:changes) { lambda { {} } }
|
||||
|
||||
it "logs a success and sends the email" do
|
||||
expect(summarizer).to receive(:record_success).with(order).once
|
||||
|
||||
subject.call
|
||||
|
||||
expect(SubscriptionMailer).to have_received(:placement_email)
|
||||
expect(mail_mock).to have_received(:deliver_now)
|
||||
end
|
||||
end
|
||||
|
||||
context "when changes are present" do
|
||||
let(:changeset) { double(:changes) }
|
||||
let(:changes) { lambda { changeset } }
|
||||
|
||||
it "logs an issue and sends the email" do
|
||||
expect(summarizer).to receive(:record_issue).with(:changes, order).once
|
||||
|
||||
subject.call
|
||||
|
||||
expect(SubscriptionMailer).to have_received(:placement_email).with(order, changeset)
|
||||
expect(mail_mock).to have_received(:deliver_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_empty_email" do
|
||||
let(:summarizer) { instance_double(OrderManagement::Subscriptions::Summarizer, record_order: true) }
|
||||
|
||||
let(:changeset) { double(:changes) }
|
||||
let(:changes) { lambda { changeset } }
|
||||
|
||||
before do
|
||||
allow(SubscriptionMailer).to receive(:empty_email) { mail_mock }
|
||||
end
|
||||
|
||||
it "logs an issue and sends the email" do
|
||||
expect(summarizer).to receive(:record_issue).with(:empty, order).once
|
||||
|
||||
subject.call
|
||||
|
||||
expect(SubscriptionMailer).to have_received(:empty_email).with(order, changeset)
|
||||
expect(mail_mock).to have_received(:deliver_now)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#move_to_completion" do
|
||||
let(:order_cycle) { create(:simple_order_cycle) }
|
||||
let(:shop) { order_cycle.coordinator }
|
||||
let(:order) { create(:order, order_cycle: order_cycle, distributor: shop) }
|
||||
let(:ex) { create(:exchange, order_cycle: order_cycle, sender: shop, receiver: shop, incoming: false) }
|
||||
let(:variant1) { create(:variant, on_hand: 5) }
|
||||
let(:variant2) { create(:variant, on_hand: 5) }
|
||||
let(:variant3) { create(:variant, on_hand: 5) }
|
||||
let!(:line_item1) { create(:line_item, order: order, variant: variant1, quantity: 3) }
|
||||
let!(:line_item2) { create(:line_item, order: order, variant: variant2, quantity: 3) }
|
||||
let!(:line_item3) { create(:line_item, order: order, variant: variant3, quantity: 3) }
|
||||
|
||||
context "and some items are not available from the order cycle" do
|
||||
before { [variant2, variant3].each { |v| ex.variants << v } }
|
||||
|
||||
context "and insufficient stock exists to fulfil the order for some items" do
|
||||
before do
|
||||
variant1.update_attribute(:on_hand, 5)
|
||||
variant2.update_attribute(:on_hand, 2)
|
||||
variant3.update_attribute(:on_hand, 0)
|
||||
end
|
||||
|
||||
context "and the order has been placed" do
|
||||
before do
|
||||
allow(order).to receive(:ensure_available_shipping_rates) { true }
|
||||
allow(order).to receive(:process_each_payment) { true }
|
||||
end
|
||||
|
||||
it "removes the unavailable items from the shipment" do
|
||||
subject.move_to_completion
|
||||
expect(order.reload.shipment.manifest.size).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user