diff --git a/app/jobs/subscription_placement_job.rb b/app/jobs/subscription_placement_job.rb index 45a8de23cb..674b6ae2f4 100644 --- a/app/jobs/subscription_placement_job.rb +++ b/app/jobs/subscription_placement_job.rb @@ -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 diff --git a/app/services/place_order.rb b/app/services/place_order.rb new file mode 100644 index 0000000000..521defdcaa --- /dev/null +++ b/app/services/place_order.rb @@ -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 diff --git a/spec/jobs/subscription_placement_job_spec.rb b/spec/jobs/subscription_placement_job_spec.rb index 06b092a400..408b6f2245 100644 --- a/spec/jobs/subscription_placement_job_spec.rb +++ b/spec/jobs/subscription_placement_job_spec.rb @@ -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 diff --git a/spec/services/place_order_spec.rb b/spec/services/place_order_spec.rb new file mode 100644 index 0000000000..191e5313e0 --- /dev/null +++ b/spec/services/place_order_spec.rb @@ -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