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:
Pau Perez
2021-03-30 12:05:52 +02:00
parent 3d65929a74
commit fcb013eb2a
4 changed files with 287 additions and 138 deletions

View File

@@ -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

View 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

View File

@@ -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

View 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