diff --git a/app/mailers/standing_order_mailer.rb b/app/mailers/standing_order_mailer.rb index 558cd1597d..050f6d7fda 100644 --- a/app/mailers/standing_order_mailer.rb +++ b/app/mailers/standing_order_mailer.rb @@ -26,6 +26,14 @@ class StandingOrderMailer < Spree::BaseMailer send_mail(order) end + def placement_summary_email(summary) + @shop = Enterprise.find(summary.shop_id) + @summary = summary + mail(:to => @shop.email, + :from => from_address, + :subject => "#{Spree::Config[:site_name]} #{t('standing_order_mailer.placement_summary_email.subject')}") + end + private def send_mail(order) diff --git a/app/views/standing_order_mailer/_summary_detail.html.haml b/app/views/standing_order_mailer/_summary_detail.html.haml new file mode 100644 index 0000000000..8f4bf7b906 --- /dev/null +++ b/app/views/standing_order_mailer/_summary_detail.html.haml @@ -0,0 +1,21 @@ +- summary.issues.each do |type, messages| + - orders = summary.orders_affected_by(type) + + %h4= t(".#{type}.title", count: orders.count) + %p= t(".#{type}.explainer") + + - separator = messages.values.any? ? ": " : ", " + - orders.each_with_index do |order, i| + %a{ href: spree.order_url(order) }>= order.number + = separator if messages.values.any? || i < orders.count - 1 + - if messages.values.any? + = messages[order.id] || t(".no_message_provided") + %br + +- if summary.unrecorded_ids.any? + - orders = summary.orders_affected_by(:other) + %h4= t(".other.title", count: orders.count) + %p= t(".other.explainer") + - orders.each_with_index do |order, i| + %a{ href: spree.order_url(order) }>= order.number + = ", " if i < orders.count - 1 diff --git a/app/views/standing_order_mailer/_summary_overview.html.haml b/app/views/standing_order_mailer/_summary_overview.html.haml new file mode 100644 index 0000000000..3f6f2af13e --- /dev/null +++ b/app/views/standing_order_mailer/_summary_overview.html.haml @@ -0,0 +1,10 @@ +%p.callout + = t(".total", count: summary.order_count) + - if summary.issue_count == 0 + = t(".success_all") + - elsif summary.issue_count < summary.order_count + = t(".success_some", count: summary.success_count) + - else + = t(".success_zero") + + = t(".issues") if summary.issue_count > 0 diff --git a/app/views/standing_order_mailer/placement_summary_email.html.haml b/app/views/standing_order_mailer/placement_summary_email.html.haml new file mode 100644 index 0000000000..579f7d331c --- /dev/null +++ b/app/views/standing_order_mailer/placement_summary_email.html.haml @@ -0,0 +1,22 @@ +%table.social.white-bg{:width => "100%"} + %tr + %td + %table.column{:align => "left"} + %tr + %td + %h3 + = t(".greeting", name: @shop.contact) + %h4 + = t(".intro") + %table.column{:align => "left"} + %tr + %td{:align => "right"} + %img.float-right{:src => "#{@shop.logo.url(:medium)}"}/ + %span.clear + += render 'summary_overview', summary: @summary += render 'summary_detail', summary: @summary + +%p   += render 'shared/mailers/signoff' += render 'shared/mailers/social_and_contact' diff --git a/config/locales/en.yml b/config/locales/en.yml index d2ddb0b687..bdc4d33039 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -107,6 +107,35 @@ en: producer_mailer: order_cycle: subject: "Order cycle report for %{producer}" + standing_order_mailer: + placement_summary_email: + subject: Your Standing Order summary + greeting: "Hi %{name}," + intro: "Below is a summary of the standing orders that have just been placed for %{shop}." + summary_overview: + total: A total of %{count} standing orders were marked for automatic placement. + success_zero: Of these, none were processed successfully. + success_some: Of these, %{count} were processed successfully. + success_all: All were processed successfully. + issues: Details of the issues encountered are provided below. + summary_detail: + no_message_provided: No error message provided + changes: + title: Insufficient Stock (%{count} orders) + explainer: These orders were processed but insufficient stock was available for some requested items + empty: + title: No stock (%{count} orders) + explainer: These orders were unable to be processed because no stock was available for any requested items + complete: + title: Already Processed (%{count} orders) + explainer: These orders were already marked as complete, and were therefore left untouched + failure: + title: Failed To Process (%{count} orders) + explainer: Automatic processing of these orders failed due to an error. The error has been listed where possible. + other: + title: Other failure (%{count} orders) + explainer: Automatic processing of these orders failed for an unknown reason. This should not occur, please contact us if you are seeing this. + home: "OFN" title: Open Food Network welcome_to: 'Welcome to ' diff --git a/lib/open_food_network/standing_order_summary.rb b/lib/open_food_network/standing_order_summary.rb index be445b9f0b..5ff418e507 100644 --- a/lib/open_food_network/standing_order_summary.rb +++ b/lib/open_food_network/standing_order_summary.rb @@ -33,5 +33,17 @@ module OpenFoodNetwork def issue_count (@order_ids - @success_ids).count end + + def orders_affected_by(type) + case type + when :other then Spree::Order.where(id: unrecorded_ids) + else Spree::Order.where(id: issues[type].keys) + end + end + + def unrecorded_ids + recorded_ids = issues.values.map(&:keys).flatten + @order_ids - @success_ids - recorded_ids + 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 index d835a85ed1..cdca0553c8 100644 --- a/spec/lib/open_food_network/standing_order_summary_spec.rb +++ b/spec/lib/open_food_network/standing_order_summary_spec.rb @@ -69,7 +69,7 @@ module OpenFoodNetwork end end - describe "#lissue_count" do + describe "#issue_count" do let(:order_ids) { [1,3,5,7,9] } let(:success_ids) { [1,2,3,4,5] } @@ -79,5 +79,47 @@ module OpenFoodNetwork expect(summary.issue_count).to be 2 # 7 & 9 end end + + describe "#orders_affected_by" do + let(:order1) { create(:order) } + let(:order2) { create(:order) } + + before do + allow(summary).to receive(:unrecorded_ids) { [order1.id] } + allow(summary).to receive(:issues) { { failure: { order2.id => "A message" } } } + end + + context "when the issue type is :other" do + let(:orders) { summary.orders_affected_by(:other) } + + it "returns orders specified by unrecorded_ids" do + expect(orders).to include order1 + expect(orders).to_not include order2 + end + end + + context "when the issue type is :other" do + let(:orders) { summary.orders_affected_by(:failure) } + + it "returns orders specified by the relevant issue hash" do + expect(orders).to include order2 + expect(orders).to_not include order1 + end + end + end + + describe "#unrecorded_ids" do + let(:issues) { { type: { 7 => "message", 8 => "message" } } } + + before do + summary.instance_variable_set(:@order_ids, [1,3,5,7,9]) + summary.instance_variable_set(:@success_ids, [1,2,3,4,5]) + summary.instance_variable_set(:@issues, issues) + end + + it "returns order_ids that are not marked as an issue or a success" do + expect(summary.unrecorded_ids).to eq [9] + end + end end end diff --git a/spec/mailers/standing_order_mailer_spec.rb b/spec/mailers/standing_order_mailer_spec.rb index ad76a99862..7be631b6a4 100644 --- a/spec/mailers/standing_order_mailer_spec.rb +++ b/spec/mailers/standing_order_mailer_spec.rb @@ -103,4 +103,114 @@ describe StandingOrderMailer do expect(body).to include "This is a payment failure error" end end + + describe "order placement summary" do + let!(:shop) { create(:enterprise) } + let!(:summary) { double(:summary, shop_id: shop.id) } + let(:body) { strip_tags(StandingOrderMailer.deliveries.last.body.encoded) } + let(:scope) { "standing_order_mailer" } + + before { allow(summary).to receive(:unrecorded_ids) { [] } } + + context "when no issues were encountered while processing standing orders" do + before do + allow(summary).to receive(:order_count) { 37 } + allow(summary).to receive(:issue_count) { 0 } + allow(summary).to receive(:issues) { {} } + StandingOrderMailer.placement_summary_email(summary).deliver + end + + it "sends the email, which notifies the enterprise that all orders were successfully processed" do + expect(body).to include I18n.t("#{scope}.placement_summary_email.intro") + expect(body).to include I18n.t("#{scope}.summary_overview.total", count: 37) + expect(body).to include I18n.t("#{scope}.summary_overview.success_all") + expect(body).to_not include I18n.t("#{scope}.summary_overview.issues") + end + end + + context "when some issues were encountered while processing standing orders" do + let(:order1) { double(:order, id: 1, number: "R123456", to_s: "R123456") } + let(:order2) { double(:order, id: 2, number: "R654321", to_s: "R654321") } + + before do + allow(summary).to receive(:order_count) { 37 } + allow(summary).to receive(:success_count) { 35 } + allow(summary).to receive(:issue_count) { 2 } + allow(summary).to receive(:issues) { { failure: { 1 => "Some Error Message", 2 => nil } } } + allow(summary).to receive(:orders_affected_by) { [order1, order2] } + end + + context "when no unrecorded issues are present" do + it "sends the email, which notifies the enterprise that some issues were encountered" do + StandingOrderMailer.placement_summary_email(summary).deliver + expect(body).to include I18n.t("#{scope}.placement_summary_email.intro") + expect(body).to include I18n.t("#{scope}.summary_overview.total", count: 37) + expect(body).to include I18n.t("#{scope}.summary_overview.success_some", count: 35) + expect(body).to include I18n.t("#{scope}.summary_overview.issues") + expect(body).to include I18n.t("#{scope}.summary_detail.failure.title", count: 2) + expect(body).to include I18n.t("#{scope}.summary_detail.failure.explainer") + + # Lists orders for which an error was encountered + expect(body).to include order1.number + expect(body).to include order2.number + + # Reports error messages provided by the summary, or default if none provided + expect(body).to include "Some Error Message" + expect(body).to include I18n.t("#{scope}.summary_detail.no_message_provided") + end + end + + context "when some undocumented orders are present" do + let(:order3) { double(:order, id: 3, number: "R333333", to_s: "R333333") } + let(:order4) { double(:order, id: 4, number: "R444444", to_s: "R444444") } + + before do + allow(summary).to receive(:unrecorded_ids) { [3, 4] } + end + + it "sends the email, which notifies the enterprise that some issues were encountered" do + expect(summary).to receive(:orders_affected_by).with(:other) { [order3, order4] } + StandingOrderMailer.placement_summary_email(summary).deliver + expect(body).to include I18n.t("#{scope}.summary_detail.failure.title", count: 2) + expect(body).to include I18n.t("#{scope}.summary_detail.failure.explainer") + expect(body).to include I18n.t("#{scope}.summary_detail.other.title", count: 2) + expect(body).to include I18n.t("#{scope}.summary_detail.other.explainer") + + # Lists orders for which no error or success was recorded + expect(body).to include order3.number + expect(body).to include order4.number + end + end + end + + context "when no standing orders were processed successfully" do + let(:order1) { double(:order, id: 1, number: "R123456", to_s: "R123456") } + let(:order2) { double(:order, id: 2, number: "R654321", to_s: "R654321") } + + before do + allow(summary).to receive(:order_count) { 2 } + allow(summary).to receive(:success_count) { 0 } + allow(summary).to receive(:issue_count) { 2 } + allow(summary).to receive(:issues) { { changes: { 1 => nil, 2 => nil } } } + allow(summary).to receive(:orders_affected_by) { [order1, order2] } + StandingOrderMailer.placement_summary_email(summary).deliver + end + + it "sends the email, which notifies the enterprise that some issues were encountered" do + expect(body).to include I18n.t("#{scope}.placement_summary_email.intro") + expect(body).to include I18n.t("#{scope}.summary_overview.total", count: 2) + expect(body).to include I18n.t("#{scope}.summary_overview.success_zero") + expect(body).to include I18n.t("#{scope}.summary_overview.issues") + expect(body).to include I18n.t("#{scope}.summary_detail.changes.title", count: 2) + expect(body).to include I18n.t("#{scope}.summary_detail.changes.explainer") + + # Lists orders for which an error was encountered + expect(body).to include order1.number + expect(body).to include order2.number + + # No error messages reported when non provided + expect(body).to_not include I18n.t("#{scope}.summary_detail.no_message_provided") + end + end + end end