From cf30b7c8830b3bdaa56d0fd2e3c708da302a39a0 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 23 Nov 2017 18:20:05 +1100 Subject: [PATCH] Add service objects for summarizing outcomes of standing order processing jobs --- .../standing_order_summarizer.rb | 43 +++++++++ .../standing_order_summary.rb | 37 +++++++ .../standing_order_summarizer_spec.rb | 96 +++++++++++++++++++ .../standing_order_summary_spec.rb | 83 ++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 lib/open_food_network/standing_order_summarizer.rb create mode 100644 lib/open_food_network/standing_order_summary.rb create mode 100644 spec/lib/open_food_network/standing_order_summarizer_spec.rb create mode 100644 spec/lib/open_food_network/standing_order_summary_spec.rb diff --git a/lib/open_food_network/standing_order_summarizer.rb b/lib/open_food_network/standing_order_summarizer.rb new file mode 100644 index 0000000000..9732a33fa8 --- /dev/null +++ b/lib/open_food_network/standing_order_summarizer.rb @@ -0,0 +1,43 @@ +require 'open_food_network/standing_order_summary' + +# Used by for StandingOrderPlacementJob and StandingOrderConfirmJob to summarize the +# result of automatic processing of standing orders for the relevant shop owners. +module OpenFoodNetwork + class StandingOrderSummarizer + def initialize + @summaries = {} + end + + def record_order(order) + summary_for(order).record_order(order) + end + + def record_success(order) + summary_for(order).record_success(order) + end + + def record_issue(type, order, message) + summary_for(order).record_issue(type, order, message) + end + + def record_failure(order) + line1 = "StandingOrderPlacementError: Cannot process order #{order.number} due to errors" + line2 = "Errors: #{order.errors.full_messages.join(', ')}" + Rails.logger.info("#{line1}\n#{line2}") + record_issue(:failure, order, line2) + end + + def send_placement_summary_emails + @summaries.values.each do |summary| + StandingOrderMailer.placement_summary_email(summary).deliver + end + end + + private + + def summary_for(order) + shop_id = order.distributor_id + @summaries[shop_id] ||= StandingOrderSummary.new(shop_id) + end + end +end diff --git a/lib/open_food_network/standing_order_summary.rb b/lib/open_food_network/standing_order_summary.rb new file mode 100644 index 0000000000..be445b9f0b --- /dev/null +++ b/lib/open_food_network/standing_order_summary.rb @@ -0,0 +1,37 @@ +module OpenFoodNetwork + class StandingOrderSummary + attr_reader :shop_id, :order_count, :success_count, :issues + + def initialize(shop_id) + @shop_id = shop_id + @order_ids = [] + @success_ids = [] + @issues = {} + end + + def record_order(order) + @order_ids << order.id + end + + def record_success(order) + @success_ids << order.id + end + + def record_issue(type, order, message) + issues[type] ||= [] + issues[type][order.id] = message + end + + def order_count + @order_ids.count + end + + def success_count + @success_ids.count + end + + def issue_count + (@order_ids - @success_ids).count + end + end +end diff --git a/spec/lib/open_food_network/standing_order_summarizer_spec.rb b/spec/lib/open_food_network/standing_order_summarizer_spec.rb new file mode 100644 index 0000000000..c01c5e0906 --- /dev/null +++ b/spec/lib/open_food_network/standing_order_summarizer_spec.rb @@ -0,0 +1,96 @@ +require 'open_food_network/standing_order_summarizer' + +module OpenFoodNetwork + describe StandingOrderSummarizer do + let(:order) { create(:order) } + let(:summarizer) { OpenFoodNetwork::StandingOrderSummarizer.new } + + describe "#summary_for" do + let(:order) { double(:order, distributor_id: 123) } + + context "when a summary for the order's distributor doesn't already exist" do + it "initializes a new summary object, and returns it" do + expect(summarizer.instance_variable_get(:@summaries).count).to be 0 + summary = summarizer.send(:summary_for, order) + expect(summary.shop_id).to be 123 + expect(summarizer.instance_variable_get(:@summaries).count).to be 1 + end + end + + context "when a summary for the order's distributor already exists" do + let(:summary) { double(:summary) } + + before do + summarizer.instance_variable_set(:@summaries, { 123 => summary }) + end + + it "returns the existing summary object" do + expect(summarizer.instance_variable_get(:@summaries).count).to be 1 + expect(summarizer.send(:summary_for, order)).to eq summary + expect(summarizer.instance_variable_get(:@summaries).count).to be 1 + end + end + end + + describe "recording events" do + let(:order) { double(:order) } + let(:summary) { double(:summary) } + before { allow(summarizer).to receive(:summary_for).with(order) { summary } } + + describe "#record_order" do + it "requests a summary for the order and calls #record_order on it" do + expect(summary).to receive(:record_order).with(order).once + summarizer.record_order(order) + end + end + + describe "#record_success" do + it "requests a summary for the order and calls #record_success on it" do + expect(summary).to receive(:record_success).with(order).once + summarizer.record_success(order) + end + end + + describe "#record_issue" do + it "requests a summary for the order and calls #record_issue on it" do + expect(summary).to receive(:record_issue).with(:type, order, "message").once + summarizer.record_issue(:type, order, "message") + end + end + + describe "#record_failure" do + before do + allow(order).to receive(:number) { "123" } + allow(order).to receive(:errors) { double(:errors, full_messages: ["Some error"]) } + allow(summarizer).to receive(:record_issue) + end + + it "sends error info to the rails logger" do + expect(Rails.logger).to receive(:info) + summarizer.record_failure(order) + end + + it "calls #record_issue on itself" do + summarizer.record_failure(order) + expect(summarizer).to have_received(:record_issue) + end + end + end + + describe "#send_placement_summary_emails" do + let(:summary1) { double(:summary) } + let(:summary2) { double(:summary) } + let(:summaries) { { 1 => summary1, 2 => summary2 } } + let(:mail_mock) { double(:mail, deliver: true) } + + before do + summarizer.instance_variable_set(:@summaries, summaries) + end + + it "sends a placement summary email for each summary" do + expect(StandingOrderMailer).to receive(:placement_summary_email).twice { mail_mock } + summarizer.send_placement_summary_emails + end + end + end +end diff --git a/spec/lib/open_food_network/standing_order_summary_spec.rb b/spec/lib/open_food_network/standing_order_summary_spec.rb new file mode 100644 index 0000000000..d835a85ed1 --- /dev/null +++ b/spec/lib/open_food_network/standing_order_summary_spec.rb @@ -0,0 +1,83 @@ +require 'open_food_network/standing_order_summary' + +module OpenFoodNetwork + describe StandingOrderSummary do + let(:summary) { OpenFoodNetwork::StandingOrderSummary.new(123) } + + describe "#initialize" do + it "initializes instance variables: shop_id, order_count, success_count and issues" do + expect(summary.shop_id).to be 123 + expect(summary.order_count).to be 0 + expect(summary.success_count).to be 0 + expect(summary.issues).to be_a Hash + end + end + + describe "#record_order" do + let(:order) { double(:order, id: 37) } + it "adds the order id to the order_ids array" do + summary.record_order(order) + expect(summary.instance_variable_get(:@order_ids)).to eq [order.id] + end + end + + describe "#record_success" do + let(:order) { double(:order, id: 37) } + it "adds the order id to the success_ids array" do + summary.record_success(order) + expect(summary.instance_variable_get(:@success_ids)).to eq [order.id] + end + end + + describe "#record_issue" do + let(:order) { double(:order, id: 1) } + + context "when no issues of the same type have been recorded yet" do + it "adds a new type to the issues hash, and stores a new issue against it" do + summary.record_issue(:some_type, order, "message") + expect(summary.issues.keys).to include :some_type + expect(summary.issues[:some_type][order.id]).to eq "message" + end + end + + context "when an issue of the same type has already been recorded" do + let(:existing_issue) { double(:existing_issue) } + + before { summary.issues[:some_type] = [existing_issue] } + + it "stores a new issue against the existing type" do + summary.record_issue(:some_type, order, "message") + expect(summary.issues[:some_type]).to include existing_issue + expect(summary.issues[:some_type][order.id]).to eq "message" + end + end + end + + describe "#order_count" do + let(:order_ids) { [1,2,3,4,5,6,7] } + it "counts the number of items in the order_ids instance_variable" do + summary.instance_variable_set(:@order_ids, order_ids) + expect(summary.order_count).to be 7 + end + end + + describe "#success_count" do + let(:success_ids) { [1,2,3,4,5,6,7] } + it "counts the number of items in the success_ids instance_variable" do + summary.instance_variable_set(:@success_ids, success_ids) + expect(summary.success_count).to be 7 + end + end + + describe "#lissue_count" do + let(:order_ids) { [1,3,5,7,9] } + let(:success_ids) { [1,2,3,4,5] } + + it "counts the number of items in order_ids that are not in success_ids" do + summary.instance_variable_set(:@order_ids, order_ids) + summary.instance_variable_set(:@success_ids, success_ids) + expect(summary.issue_count).to be 2 # 7 & 9 + end + end + end +end