From caf1c9ecd94967560da17a4feceed211f1a60cb1 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 13 Jan 2021 16:35:04 +0100 Subject: [PATCH 01/13] Move data fetching from injection helper to action Data fetching is a controller action responsibility. We shouldn't couple the controller with it too much but it should trigger it, not the view-layer. --- app/controllers/spree/users_controller.rb | 5 +++++ app/views/spree/users/show.html.haml | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/spree/users_controller.rb b/app/controllers/spree/users_controller.rb index 1fb03af1f9..5be52a0910 100644 --- a/app/controllers/spree/users_controller.rb +++ b/app/controllers/spree/users_controller.rb @@ -15,6 +15,11 @@ module Spree # Ignores invoice orders, only order where state: 'complete' def show @orders = @user.orders.where(state: 'complete').order('completed_at desc') + + customers = spree_current_user.customers + @shops = Enterprise + .where(id: @orders.pluck(:distributor_id).uniq | customers.pluck(:enterprise_id)) + @unconfirmed_email = spree_current_user.unconfirmed_email end diff --git a/app/views/spree/users/show.html.haml b/app/views/spree/users/show.html.haml index bddce94ce9..ec81685156 100644 --- a/app/views/spree/users/show.html.haml +++ b/app/views/spree/users/show.html.haml @@ -1,7 +1,8 @@ - content_for :injection_data do - = inject_orders - = inject_shops + = inject_json_array("orders", @orders.all, Api::OrderSerializer) + = inject_json_array("shops", @shops.all, Api::ShopForOrdersSerializer) = inject_saved_credit_cards + - if Stripe.publishable_key :javascript angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}")) From 681a009eb61232fe671d680b7c1780d814209175 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 13 Jan 2021 16:37:43 +0100 Subject: [PATCH 02/13] Extract outstanding balance SQL CASE/WHEN --- app/controllers/spree/users_controller.rb | 8 +++++-- app/models/spree/order.rb | 8 +++++++ app/queries/outstanding_balance.rb | 27 +++++++++++++++++++++++ app/services/customers_with_balance.rb | 6 ++--- 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 app/queries/outstanding_balance.rb diff --git a/app/controllers/spree/users_controller.rb b/app/controllers/spree/users_controller.rb index 5be52a0910..a4029d33cb 100644 --- a/app/controllers/spree/users_controller.rb +++ b/app/controllers/spree/users_controller.rb @@ -12,9 +12,13 @@ module Spree before_action :set_locale before_action :enable_embedded_shopfront - # Ignores invoice orders, only order where state: 'complete' + # Ignores invoice orders def show - @orders = @user.orders.where(state: 'complete').order('completed_at desc') + @orders = @user.orders + .where.not(Spree::Order.in_incomplete_state.where_values_hash) + .select('spree_orders.*') + .select(OutstandingBalance.new.query) + .order('completed_at desc') customers = spree_current_user.customers @shops = Enterprise diff --git a/app/models/spree/order.rb b/app/models/spree/order.rb index 07cd78dac7..ce279ec58b 100644 --- a/app/models/spree/order.rb +++ b/app/models/spree/order.rb @@ -130,6 +130,14 @@ module Spree where("state != ?", state) } + # All the states an order can be in before completing the checkout + PRIOR_TO_COMPLETION_STATES = %w(cart address delivery payment).freeze + # All the states of a complete order but that shouldn't count towards the balance. Those that the + # customer won't enjoy. + NON_FULFILLED_STATES = %w(canceled returned).freeze + + scope :in_incomplete_state, -> { where(state: PRIOR_TO_COMPLETION_STATES) } + def self.by_number(number) where(number: number) end diff --git a/app/queries/outstanding_balance.rb b/app/queries/outstanding_balance.rb new file mode 100644 index 0000000000..cc9ba7ab58 --- /dev/null +++ b/app/queries/outstanding_balance.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Encapsulates the SQL statement that computes the balance of an order as a new column in the result +# set. This can then be reused chaining it with the ActiveRecord::Relation objects you pass in the +# constructor. +# +# Alternatively, you can get the SQL by calling #statement, which is suitable for more complex +# cases. +# +# See CompleteOrdersWithBalance or CustomersWithBalance as examples. +class OutstandingBalance + def query + <<-SQL.strip_heredoc + CASE WHEN state IN #{non_fulfilled_states_group.to_sql} THEN payment_total + WHEN state IS NOT NULL THEN payment_total - total + ELSE 0 END + AS balance_value + SQL + end + + private + + def non_fulfilled_states_group + states = Spree::Order::NON_FULFILLED_STATES.map { |state| Arel::Nodes.build_quoted(state) } + Arel::Nodes::Grouping.new(states) + end +end diff --git a/app/services/customers_with_balance.rb b/app/services/customers_with_balance.rb index 56a3c2d171..5548fa4531 100644 --- a/app/services/customers_with_balance.rb +++ b/app/services/customers_with_balance.rb @@ -39,12 +39,12 @@ class CustomersWithBalance end def complete_orders - states_group = prior_to_completion_states.map { |state| Arel::Nodes.build_quoted(state) } - Arel::Nodes::NotIn.new(Spree::Order.arel_table[:state], states_group) + states = Spree::Order::PRIOR_TO_COMPLETION_STATES.map { |state| Arel::Nodes.build_quoted(state) } + Arel::Nodes::NotIn.new(Spree::Order.arel_table[:state], states) end def non_fulfilled_states_group - states_group = non_fulfilled_states.map { |state| Arel::Nodes.build_quoted(state) } + states_group = Spree::Order::NON_FULFILLED_STATES.map { |state| Arel::Nodes.build_quoted(state) } Arel::Nodes::Grouping.new(states_group) end From 20abaaa950cd3afb8211b19d8661d286f47cd8db Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 13 Jan 2021 14:28:30 +0100 Subject: [PATCH 03/13] Reuse outstanding balance statement across queries Instead of relying on Spree::Order#outstanding_balance we make us of the result set `balance_value` computed column. So, we ask PostgreSQL to compute it instead of Ruby and then serialize it from that computed column. That's a bit faster to compute that way and let's reuse logic. We hide this new implementation under this features' toggle so it's only used when enabled. We want hit the old behaviour by default. --- app/controllers/spree/users_controller.rb | 3 +- app/models/spree/order.rb | 4 +-- app/queries/outstanding_balance.rb | 17 +++++++++-- app/serializers/api/order_serializer.rb | 8 +++++ app/services/customers_with_balance.rb | 30 ++----------------- spec/serializers/api/order_serializer_spec.rb | 26 ++++++++++++++++ 6 files changed, 56 insertions(+), 32 deletions(-) diff --git a/app/controllers/spree/users_controller.rb b/app/controllers/spree/users_controller.rb index a4029d33cb..7d46be6748 100644 --- a/app/controllers/spree/users_controller.rb +++ b/app/controllers/spree/users_controller.rb @@ -17,9 +17,10 @@ module Spree @orders = @user.orders .where.not(Spree::Order.in_incomplete_state.where_values_hash) .select('spree_orders.*') - .select(OutstandingBalance.new.query) .order('completed_at desc') + @orders = OutstandingBalance.new(@orders).query + customers = spree_current_user.customers @shops = Enterprise .where(id: @orders.pluck(:distributor_id).uniq | customers.pluck(:enterprise_id)) diff --git a/app/models/spree/order.rb b/app/models/spree/order.rb index ce279ec58b..a908c56e1e 100644 --- a/app/models/spree/order.rb +++ b/app/models/spree/order.rb @@ -132,8 +132,8 @@ module Spree # All the states an order can be in before completing the checkout PRIOR_TO_COMPLETION_STATES = %w(cart address delivery payment).freeze - # All the states of a complete order but that shouldn't count towards the balance. Those that the - # customer won't enjoy. + # All the states of a complete order but that shouldn't count towards the balance. Those that + # the customer won't enjoy. NON_FULFILLED_STATES = %w(canceled returned).freeze scope :in_incomplete_state, -> { where(state: PRIOR_TO_COMPLETION_STATES) } diff --git a/app/queries/outstanding_balance.rb b/app/queries/outstanding_balance.rb index cc9ba7ab58..a31bb1b2df 100644 --- a/app/queries/outstanding_balance.rb +++ b/app/queries/outstanding_balance.rb @@ -9,17 +9,30 @@ # # See CompleteOrdersWithBalance or CustomersWithBalance as examples. class OutstandingBalance + # The relation must be an ActiveRecord::Relation object with `spree_orders` in the SQL statement + # FROM for #statement to work. + def initialize(relation = nil) + @relation = relation + end + def query + relation.select("#{statement} AS balance_value") + end + + # Arel doesn't support CASE statements until v7.1.0 so we'll have to wait with SQL literals + # a little longer. See https://github.com/rails/arel/pull/400 for details. + def statement <<-SQL.strip_heredoc CASE WHEN state IN #{non_fulfilled_states_group.to_sql} THEN payment_total - WHEN state IS NOT NULL THEN payment_total - total + WHEN state IS NOT NULL THEN payment_total - total ELSE 0 END - AS balance_value SQL end private + attr_reader :relation + def non_fulfilled_states_group states = Spree::Order::NON_FULFILLED_STATES.map { |state| Arel::Nodes.build_quoted(state) } Arel::Nodes::Grouping.new(states) diff --git a/app/serializers/api/order_serializer.rb b/app/serializers/api/order_serializer.rb index 427c222ea2..73532a1704 100644 --- a/app/serializers/api/order_serializer.rb +++ b/app/serializers/api/order_serializer.rb @@ -7,6 +7,14 @@ module Api has_many :payments, serializer: Api::PaymentSerializer + def outstanding_balance + if OpenFoodNetwork::FeatureToggle.enabled?(:customer_balance, object.user) + -object.balance_value + else + object.outstanding_balance + end + end + def payments object.payments.joins(:payment_method).completed end diff --git a/app/services/customers_with_balance.rb b/app/services/customers_with_balance.rb index 5548fa4531..bf6cf70396 100644 --- a/app/services/customers_with_balance.rb +++ b/app/services/customers_with_balance.rb @@ -10,23 +10,15 @@ class CustomersWithBalance joins(left_join_complete_orders). group("customers.id"). select("customers.*"). - select(outstanding_balance) + select(outstanding_balance_sum) end private attr_reader :enterprise - # Arel doesn't support CASE statements until v7.1.0 so we'll have to wait with SQL literals - # a little longer. See https://github.com/rails/arel/pull/400 for details. - def outstanding_balance - <<-SQL.strip_heredoc - SUM( - CASE WHEN state IN #{non_fulfilled_states_group.to_sql} THEN payment_total - WHEN state IS NOT NULL THEN payment_total - total - ELSE 0 END - ) AS balance_value - SQL + def outstanding_balance_sum + "SUM(#{OutstandingBalance.new.statement}) AS balance_value" end # The resulting orders are in states that belong after the checkout. Only these can be considered @@ -42,20 +34,4 @@ class CustomersWithBalance states = Spree::Order::PRIOR_TO_COMPLETION_STATES.map { |state| Arel::Nodes.build_quoted(state) } Arel::Nodes::NotIn.new(Spree::Order.arel_table[:state], states) end - - def non_fulfilled_states_group - states_group = Spree::Order::NON_FULFILLED_STATES.map { |state| Arel::Nodes.build_quoted(state) } - Arel::Nodes::Grouping.new(states_group) - end - - # All the states an order can be in before completing the checkout - def prior_to_completion_states - %w(cart address delivery payment) - end - - # All the states of a complete order but that shouldn't count towards the balance. Those that the - # customer won't enjoy. - def non_fulfilled_states - %w(canceled returned) - end end diff --git a/spec/serializers/api/order_serializer_spec.rb b/spec/serializers/api/order_serializer_spec.rb index 04ef728d59..9b32b754e4 100644 --- a/spec/serializers/api/order_serializer_spec.rb +++ b/spec/serializers/api/order_serializer_spec.rb @@ -23,4 +23,30 @@ describe Api::OrderSerializer do expect(serializer.to_json).to match completed_payment.amount.to_s expect(serializer.to_json).to_not match payment.amount.to_s end + + describe '#outstanding_balance' do + context 'when the customer_balance feature is enabled' do + before do + allow(OpenFoodNetwork::FeatureToggle) + .to receive(:enabled?).with(:customer_balance, order.user) { true } + + allow(order).to receive(:balance_value).and_return(-1.23) + end + + it "returns the object's balance_value from the users perspective" do + expect(serializer.serializable_hash[:outstanding_balance]).to eq(1.23) + end + end + + context 'when the customer_balance is not enabled' do + before do + allow(OpenFoodNetwork::FeatureToggle) + .to receive(:enabled?).with(:customer_balance, order.user) { false } + end + + it 'calls #outstanding_balance on the object' do + expect(serializer.serializable_hash[:outstanding_balance]).to eq(1.0) + end + end + end end From a124f93b20353b8dd769e8ff18c58d445222c903 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 13 Jan 2021 17:33:50 +0100 Subject: [PATCH 04/13] Remove old commented out code --- spec/serializers/api/order_serializer_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/serializers/api/order_serializer_spec.rb b/spec/serializers/api/order_serializer_spec.rb index 9b32b754e4..bc1115fc90 100644 --- a/spec/serializers/api/order_serializer_spec.rb +++ b/spec/serializers/api/order_serializer_spec.rb @@ -14,7 +14,6 @@ describe Api::OrderSerializer do end it "convert the state attributes to translatable keys" do - # byebug if serializer.to_json =~ /balance_due/ expect(serializer.to_json).to match "complete" expect(serializer.to_json).to match "balance_due" end @@ -42,10 +41,12 @@ describe Api::OrderSerializer do before do allow(OpenFoodNetwork::FeatureToggle) .to receive(:enabled?).with(:customer_balance, order.user) { false } + + allow(order).to receive(:outstanding_balance).and_return(123.0) end it 'calls #outstanding_balance on the object' do - expect(serializer.serializable_hash[:outstanding_balance]).to eq(1.0) + expect(serializer.serializable_hash[:outstanding_balance]).to eq(123.0) end end end From 37b7340eb134bc723783fc2fc56f2a00664f6c4d Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 13 Jan 2021 17:41:16 +0100 Subject: [PATCH 05/13] Refactor specs to speed them up We don't care about the conversion from hash to JSON (that's an ActiveModel::Serializer responsibility that is thoroughly tested) but our logic so we can skip that step which only slows down tests. It consistently reduced ~1.5s on my machine but it's still too slow to wait ~8.5s to get feedback from them. --- spec/serializers/api/order_serializer_spec.rb | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/spec/serializers/api/order_serializer_spec.rb b/spec/serializers/api/order_serializer_spec.rb index bc1115fc90..d635cf0ada 100644 --- a/spec/serializers/api/order_serializer_spec.rb +++ b/spec/serializers/api/order_serializer_spec.rb @@ -6,21 +6,28 @@ describe Api::OrderSerializer do let(:serializer) { Api::OrderSerializer.new order } let(:order) { create(:completed_order_with_totals) } - let!(:completed_payment) { create(:payment, order: order, state: 'completed', amount: order.total - 1) } - let!(:payment) { create(:payment, order: order, state: 'checkout', amount: 123.45) } + describe '#serializable_hash' do + let!(:completed_payment) do + create(:payment, order: order, state: 'completed', amount: order.total - 1) + end + let!(:payment) { create(:payment, order: order, state: 'checkout', amount: 123.45) } - it "serializes an order" do - expect(serializer.to_json).to match order.number.to_s - end + it "serializes an order" do + expect(serializer.serializable_hash[:number]).to eq(order.number) + end - it "convert the state attributes to translatable keys" do - expect(serializer.to_json).to match "complete" - expect(serializer.to_json).to match "balance_due" - end + it "convert the state attributes to translatable keys" do + hash = serializer.serializable_hash - it "only serializes completed payments" do - expect(serializer.to_json).to match completed_payment.amount.to_s - expect(serializer.to_json).to_not match payment.amount.to_s + expect(hash[:state]).to eq("complete") + expect(hash[:payment_state]).to eq("balance_due") + end + + it "only serializes completed payments" do + hash = serializer.serializable_hash + + expect(hash[:payments].first[:amount]).to eq(completed_payment.amount) + end end describe '#outstanding_balance' do From e8ef4acb2b2fd15d28de8b51312eb6a53ff2c20a Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 13 Jan 2021 19:13:27 +0100 Subject: [PATCH 06/13] Hide new data fetching implementation under toggle --- app/controllers/spree/users_controller.rb | 20 +++++++++++++------ .../spree/users_controller_spec.rb | 16 +++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/controllers/spree/users_controller.rb b/app/controllers/spree/users_controller.rb index 7d46be6748..2f7722c401 100644 --- a/app/controllers/spree/users_controller.rb +++ b/app/controllers/spree/users_controller.rb @@ -14,12 +14,7 @@ module Spree # Ignores invoice orders def show - @orders = @user.orders - .where.not(Spree::Order.in_incomplete_state.where_values_hash) - .select('spree_orders.*') - .order('completed_at desc') - - @orders = OutstandingBalance.new(@orders).query + @orders = orders_collection customers = spree_current_user.customers @shops = Enterprise @@ -64,6 +59,19 @@ module Spree private + def orders_collection + if OpenFoodNetwork::FeatureToggle.enabled?(:customer_balance, spree_current_user) + orders = @user.orders + .where.not(Spree::Order.in_incomplete_state.where_values_hash) + .select('spree_orders.*') + .order('completed_at desc') + + OutstandingBalance.new(orders).query + else + @user.orders.where(state: 'complete').order('completed_at desc') + end + end + def load_object @user ||= spree_current_user if @user diff --git a/spec/controllers/spree/users_controller_spec.rb b/spec/controllers/spree/users_controller_spec.rb index 8c5c6b80b1..8f7b4257c7 100644 --- a/spec/controllers/spree/users_controller_spec.rb +++ b/spec/controllers/spree/users_controller_spec.rb @@ -40,6 +40,22 @@ describe Spree::UsersController, type: :controller do # Doesn't return uncompleted orders" do expect(orders).not_to include d1o3 end + + context 'when the customer_balance feature is enabled' do + let(:outstanding_balance) { double(:outstanding_balance) } + + before do + allow(OpenFoodNetwork::FeatureToggle) + .to receive(:enabled?).with(:customer_balance, controller.spree_current_user) { true } + end + + it 'calls OutstandingBalance' do + allow(OutstandingBalance).to receive(:new).and_return(outstanding_balance) + expect(outstanding_balance).to receive(:query) { Spree::Order.none } + + spree_get :show + end + end end describe "registered_email" do From d18e79ab19a4a40e8c1e54f52ff0ecfdbb2838da Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 14 Jan 2021 10:47:25 +0100 Subject: [PATCH 07/13] Move query object to app/queries/ and doc it --- app/{services => queries}/customers_with_balance.rb | 2 ++ spec/{services => queries}/customers_with_balance_spec.rb | 0 2 files changed, 2 insertions(+) rename app/{services => queries}/customers_with_balance.rb (83%) rename spec/{services => queries}/customers_with_balance_spec.rb (100%) diff --git a/app/services/customers_with_balance.rb b/app/queries/customers_with_balance.rb similarity index 83% rename from app/services/customers_with_balance.rb rename to app/queries/customers_with_balance.rb index bf6cf70396..2303c28687 100644 --- a/app/services/customers_with_balance.rb +++ b/app/queries/customers_with_balance.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# Fetches the customers of the specified enterprise including the aggregated balance across the +# customer's orders. That is, we get the total balance for each customer with this enterprise. class CustomersWithBalance def initialize(enterprise) @enterprise = enterprise diff --git a/spec/services/customers_with_balance_spec.rb b/spec/queries/customers_with_balance_spec.rb similarity index 100% rename from spec/services/customers_with_balance_spec.rb rename to spec/queries/customers_with_balance_spec.rb From 783863056d75779c5332c77384ad39828ea9b025 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Thu, 14 Jan 2021 10:54:28 +0100 Subject: [PATCH 08/13] Extract query object out of UsersController It improves the overall readability of the code and as a result, things became easier to manage already. --- app/controllers/spree/users_controller.rb | 7 +-- app/queries/complete_orders_with_balance.rb | 21 +++++++ .../complete_orders_with_balance_spec.rb | 61 +++++++++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 app/queries/complete_orders_with_balance.rb create mode 100644 spec/queries/complete_orders_with_balance_spec.rb diff --git a/app/controllers/spree/users_controller.rb b/app/controllers/spree/users_controller.rb index 2f7722c401..1d9931410b 100644 --- a/app/controllers/spree/users_controller.rb +++ b/app/controllers/spree/users_controller.rb @@ -61,12 +61,7 @@ module Spree def orders_collection if OpenFoodNetwork::FeatureToggle.enabled?(:customer_balance, spree_current_user) - orders = @user.orders - .where.not(Spree::Order.in_incomplete_state.where_values_hash) - .select('spree_orders.*') - .order('completed_at desc') - - OutstandingBalance.new(orders).query + CompleteOrdersWithBalance.new(@user).query else @user.orders.where(state: 'complete').order('completed_at desc') end diff --git a/app/queries/complete_orders_with_balance.rb b/app/queries/complete_orders_with_balance.rb new file mode 100644 index 0000000000..31e89d69fb --- /dev/null +++ b/app/queries/complete_orders_with_balance.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Fetches complete orders of the specified user including their balance as a computed column +class CompleteOrdersWithBalance + def initialize(user) + @user = user + end + + def query + OutstandingBalance.new(sorted_complete_orders).query + end + + private + + def sorted_complete_orders + @user.orders + .where.not(Spree::Order.in_incomplete_state.where_values_hash) + .select('spree_orders.*') + .order(completed_at: :desc) + end +end diff --git a/spec/queries/complete_orders_with_balance_spec.rb b/spec/queries/complete_orders_with_balance_spec.rb new file mode 100644 index 0000000000..9d46feb7da --- /dev/null +++ b/spec/queries/complete_orders_with_balance_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe CompleteOrdersWithBalance do + let(:complete_orders_with_balance) { described_class.new(user) } + + describe '#query' do + let(:user) { order.user } + let(:outstanding_balance) { instance_double(OutstandingBalance) } + + context 'when the user has complete orders' do + let(:order) do + create(:order, state: 'complete', total: 2.0, payment_total: 1.0, completed_at: 2.day.ago) + end + let!(:other_order) do + create( + :order, + user: user, + state: 'complete', + total: 2.0, + payment_total: 1.0, + completed_at: 1.days.ago + ) + end + + it 'calls OutstandingBalance#query' do + allow(OutstandingBalance).to receive(:new).and_return(outstanding_balance) + expect(outstanding_balance).to receive(:query) + + complete_orders_with_balance.query + end + + it 'returns complete orders including their balance' do + order = complete_orders_with_balance.query.first + expect(order[:balance_value]).to eq(-1.0) + end + + it 'sorts them by their completed_at with the most recent first' do + orders = complete_orders_with_balance.query + expect(orders.pluck(:id)).to eq([other_order.id, order.id]) + end + end + + context 'when the user has no complete orders' do + let(:order) { create(:order) } + + it 'calls OutstandingBalance' do + allow(OutstandingBalance).to receive(:new).and_return(outstanding_balance) + expect(outstanding_balance).to receive(:query) + + complete_orders_with_balance.query + end + + it 'returns an empty array' do + order = complete_orders_with_balance.query + expect(order).to be_empty + end + end + end +end From db23428832e3df2a5adb255f2183711fc8a0e460 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 18 Jan 2021 11:06:19 +0100 Subject: [PATCH 09/13] Test OutstandingBalance This duplicates the scenarios tested for CustomersWithBalance. --- spec/queries/outstanding_balance_spec.rb | 197 +++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 spec/queries/outstanding_balance_spec.rb diff --git a/spec/queries/outstanding_balance_spec.rb b/spec/queries/outstanding_balance_spec.rb new file mode 100644 index 0000000000..a662bcccf9 --- /dev/null +++ b/spec/queries/outstanding_balance_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe OutstandingBalance do + let(:outstanding_balance) { described_class.new(relation) } + + describe '#statement' do + let(:relation) { Spree::Order.none } + + it 'returns the CASE statement necessary to compute the order balance' do + normalized_sql_statement = normalize(outstanding_balance.statement) + + expect(normalized_sql_statement).to eq(normalize(<<-SQL)) + CASE WHEN state IN ('canceled', 'returned') THEN payment_total + WHEN state IS NOT NULL THEN payment_total - total + ELSE 0 END + SQL + end + + def normalize(sql) + sql.strip_heredoc.gsub("\n", '').squeeze(' ') + end + end + + describe '#query' do + let(:relation) { Spree::Order.all } + let(:total) { 200.00 } + let(:order_total) { 100.00 } + + context 'when orders are in cart state' do + before do + create(:order, total: order_total, payment_total: 0, state: 'cart') + create(:order, total: order_total, payment_total: 0, state: 'cart') + end + + it 'returns the order balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(-order_total) + end + end + + context 'when orders are in address state' do + before do + create(:order, total: order_total, payment_total: 0, state: 'address') + create(:order, total: order_total, payment_total: 50, state: 'address') + end + + it 'returns the order balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(-order_total) + end + end + + context 'when orders are in delivery state' do + before do + create(:order, total: order_total, payment_total: 0, state: 'delivery') + create(:order, total: order_total, payment_total: 50, state: 'delivery') + end + + it 'returns the order balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(-order_total) + end + end + + context 'when orders are in payment state' do + before do + create(:order, total: order_total, payment_total: 0, state: 'payment') + create(:order, total: order_total, payment_total: 50, state: 'payment') + end + + it 'returns the order balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(-order_total) + end + end + + context 'when no orders where paid' do + before do + order = create(:order, total: order_total, payment_total: 0) + order.update_attribute(:state, 'checkout') + order = create(:order, total: order_total, payment_total: 0) + order.update_attribute(:state, 'checkout') + end + + it 'returns the customer balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(-order_total) + end + end + + context 'when an order was paid' do + let(:payment_total) { order_total } + + before do + order = create(:order, total: order_total, payment_total: 0) + order.update_attribute(:state, 'checkout') + order = create(:order, total: order_total, payment_total: payment_total) + order.update_attribute(:state, 'checkout') + end + + it 'returns the customer balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(payment_total - 200.0) + end + end + + context 'when an order is canceled' do + let(:payment_total) { order_total } + let(:non_canceled_orders_total) { order_total } + + before do + create(:order, total: order_total, payment_total: order_total, state: 'canceled') + order = create(:order, total: order_total, payment_total: 0) + order.update_attribute(:state, 'checkout') + end + + it 'returns the customer balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(payment_total) + end + end + + context 'when an order is resumed' do + let(:payment_total) { order_total } + + before do + order = create(:order, total: order_total, payment_total: 0) + order.update_attribute(:state, 'checkout') + order = create(:order, total: order_total, payment_total: payment_total) + order.update_attribute(:state, 'resumed') + end + + it 'returns the customer balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(payment_total - 200.0) + end + end + + context 'when an order is in payment' do + let(:payment_total) { order_total } + + before do + order = create(:order, total: order_total, payment_total: 0) + order.update_attribute(:state, 'checkout') + order = create(:order, total: order_total, payment_total: payment_total) + order.update_attribute(:state, 'payment') + end + + it 'returns the customer balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(payment_total - 200.0) + end + end + + context 'when an order is awaiting_return' do + let(:payment_total) { order_total } + + before do + order = create(:order, total: order_total, payment_total: 0) + order.update_attribute(:state, 'checkout') + order = create(:order, total: order_total, payment_total: payment_total) + order.update_attribute(:state, 'awaiting_return') + end + + it 'returns the customer balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(payment_total - 200.0) + end + end + + context 'when an order is returned' do + let(:payment_total) { order_total } + let(:non_returned_orders_total) { order_total } + + before do + order = create(:order, total: order_total, payment_total: payment_total) + order.update_attribute(:state, 'returned') + order = create(:order, total: order_total, payment_total: 0) + order.update_attribute(:state, 'checkout') + end + + it 'returns the customer balance' do + order = outstanding_balance.query.first + expect(order.balance_value).to eq(payment_total) + end + end + + context 'when there are no orders' do + it 'returns the order balance' do + orders = outstanding_balance.query + expect(orders).to be_empty + end + end + end +end From 9bb49bb59005dc2707ab805690010e8d5d7a3242 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Mon, 18 Jan 2021 17:13:21 +0100 Subject: [PATCH 10/13] Ensure the query the class depends on is called --- spec/queries/customers_with_balance_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/queries/customers_with_balance_spec.rb b/spec/queries/customers_with_balance_spec.rb index fefd1d9090..7942e36060 100644 --- a/spec/queries/customers_with_balance_spec.rb +++ b/spec/queries/customers_with_balance_spec.rb @@ -9,6 +9,14 @@ describe CustomersWithBalance do let(:customer) { create(:customer) } let(:total) { 200.00 } let(:order_total) { 100.00 } + let(:outstanding_balance) { instance_double(OutstandingBalance) } + + it 'calls CustomersWithBalance#statement' do + allow(OutstandingBalance).to receive(:new).and_return(outstanding_balance) + expect(outstanding_balance).to receive(:statement) + + customers_with_balance.query + end context 'when orders are in cart state' do before do From 996761da67ec8924daf7d47166ff4c34a368ed74 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Tue, 19 Jan 2021 13:26:32 +0100 Subject: [PATCH 11/13] Fix long line --- app/queries/customers_with_balance.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/queries/customers_with_balance.rb b/app/queries/customers_with_balance.rb index 2303c28687..0d547f6c2c 100644 --- a/app/queries/customers_with_balance.rb +++ b/app/queries/customers_with_balance.rb @@ -33,7 +33,8 @@ class CustomersWithBalance end def complete_orders - states = Spree::Order::PRIOR_TO_COMPLETION_STATES.map { |state| Arel::Nodes.build_quoted(state) } + states = Spree::Order::PRIOR_TO_COMPLETION_STATES + .map { |state| Arel::Nodes.build_quoted(state) } Arel::Nodes::NotIn.new(Spree::Order.arel_table[:state], states) end end From d9c065a3111a253f192b6af803a92fd4b37dc14d Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 20 Jan 2021 13:08:06 +0100 Subject: [PATCH 12/13] Remove outdated comment This comment was related to the feature we removed in https://github.com/openfoodfoundation/openfoodnetwork/pull/3609. --- app/controllers/spree/users_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/spree/users_controller.rb b/app/controllers/spree/users_controller.rb index 1d9931410b..c9ee2e15fe 100644 --- a/app/controllers/spree/users_controller.rb +++ b/app/controllers/spree/users_controller.rb @@ -12,7 +12,6 @@ module Spree before_action :set_locale before_action :enable_embedded_shopfront - # Ignores invoice orders def show @orders = orders_collection From cc9e3fe69bf14ab34db0930a7eee47b791642180 Mon Sep 17 00:00:00 2001 From: Pau Perez Date: Wed, 20 Jan 2021 14:24:59 +0100 Subject: [PATCH 13/13] Replace double negation with proper list of states We rely now on the exhaustive list of states an order can be in after checkout. What made this all a bit more messy is that I made up the "checkout" order state, likely mixing it from the payment model states. This simplifies things quite a bit and gives meaningful names to things. --- app/models/spree/order.rb | 9 +++------ app/queries/complete_orders_with_balance.rb | 6 +++--- app/queries/customers_with_balance.rb | 9 ++++----- app/queries/outstanding_balance.rb | 6 +++++- spec/queries/customers_with_balance_spec.rb | 18 +++++++++--------- spec/queries/outstanding_balance_spec.rb | 18 +++++++++--------- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/app/models/spree/order.rb b/app/models/spree/order.rb index a908c56e1e..ac75ff2fe1 100644 --- a/app/models/spree/order.rb +++ b/app/models/spree/order.rb @@ -130,13 +130,10 @@ module Spree where("state != ?", state) } - # All the states an order can be in before completing the checkout - PRIOR_TO_COMPLETION_STATES = %w(cart address delivery payment).freeze - # All the states of a complete order but that shouldn't count towards the balance. Those that - # the customer won't enjoy. - NON_FULFILLED_STATES = %w(canceled returned).freeze + # All the states an order can be in after completing the checkout + FINALIZED_STATES = %w(complete canceled resumed awaiting_return returned).freeze - scope :in_incomplete_state, -> { where(state: PRIOR_TO_COMPLETION_STATES) } + scope :finalized, -> { where(state: FINALIZED_STATES) } def self.by_number(number) where(number: number) diff --git a/app/queries/complete_orders_with_balance.rb b/app/queries/complete_orders_with_balance.rb index 31e89d69fb..57223e5c6e 100644 --- a/app/queries/complete_orders_with_balance.rb +++ b/app/queries/complete_orders_with_balance.rb @@ -7,14 +7,14 @@ class CompleteOrdersWithBalance end def query - OutstandingBalance.new(sorted_complete_orders).query + OutstandingBalance.new(sorted_finalized_orders).query end private - def sorted_complete_orders + def sorted_finalized_orders @user.orders - .where.not(Spree::Order.in_incomplete_state.where_values_hash) + .finalized .select('spree_orders.*') .order(completed_at: :desc) end diff --git a/app/queries/customers_with_balance.rb b/app/queries/customers_with_balance.rb index 0d547f6c2c..4abd070d34 100644 --- a/app/queries/customers_with_balance.rb +++ b/app/queries/customers_with_balance.rb @@ -28,13 +28,12 @@ class CustomersWithBalance def left_join_complete_orders <<-SQL.strip_heredoc LEFT JOIN spree_orders ON spree_orders.customer_id = customers.id - AND #{complete_orders.to_sql} + AND #{finalized_states.to_sql} SQL end - def complete_orders - states = Spree::Order::PRIOR_TO_COMPLETION_STATES - .map { |state| Arel::Nodes.build_quoted(state) } - Arel::Nodes::NotIn.new(Spree::Order.arel_table[:state], states) + def finalized_states + states = Spree::Order::FINALIZED_STATES.map { |state| Arel::Nodes.build_quoted(state) } + Arel::Nodes::In.new(Spree::Order.arel_table[:state], states) end end diff --git a/app/queries/outstanding_balance.rb b/app/queries/outstanding_balance.rb index a31bb1b2df..ce4d4d78f4 100644 --- a/app/queries/outstanding_balance.rb +++ b/app/queries/outstanding_balance.rb @@ -9,6 +9,10 @@ # # See CompleteOrdersWithBalance or CustomersWithBalance as examples. class OutstandingBalance + # All the states of a finished order but that shouldn't count towards the balance (the customer + # didn't get the order for whatever reason). Note it does not include complete + FINALIZED_NON_SUCCESSFUL_STATES = %w(canceled returned).freeze + # The relation must be an ActiveRecord::Relation object with `spree_orders` in the SQL statement # FROM for #statement to work. def initialize(relation = nil) @@ -34,7 +38,7 @@ class OutstandingBalance attr_reader :relation def non_fulfilled_states_group - states = Spree::Order::NON_FULFILLED_STATES.map { |state| Arel::Nodes.build_quoted(state) } + states = FINALIZED_NON_SUCCESSFUL_STATES.map { |state| Arel::Nodes.build_quoted(state) } Arel::Nodes::Grouping.new(states) end end diff --git a/spec/queries/customers_with_balance_spec.rb b/spec/queries/customers_with_balance_spec.rb index 7942e36060..479be6b410 100644 --- a/spec/queries/customers_with_balance_spec.rb +++ b/spec/queries/customers_with_balance_spec.rb @@ -69,9 +69,9 @@ describe CustomersWithBalance do context 'when no orders where paid' do before do order = create(:order, customer: customer, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, customer: customer, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') end it 'returns the customer balance' do @@ -85,9 +85,9 @@ describe CustomersWithBalance do before do order = create(:order, customer: customer, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, customer: customer, total: order_total, payment_total: payment_total) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') end it 'returns the customer balance' do @@ -102,7 +102,7 @@ describe CustomersWithBalance do before do order = create(:order, customer: customer, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') create( :order, customer: customer, @@ -123,7 +123,7 @@ describe CustomersWithBalance do before do order = create(:order, customer: customer, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, customer: customer, total: order_total, payment_total: payment_total) order.update_attribute(:state, 'resumed') end @@ -139,7 +139,7 @@ describe CustomersWithBalance do before do order = create(:order, customer: customer, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, customer: customer, total: order_total, payment_total: payment_total) order.update_attribute(:state, 'payment') end @@ -155,7 +155,7 @@ describe CustomersWithBalance do before do order = create(:order, customer: customer, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, customer: customer, total: order_total, payment_total: payment_total) order.update_attribute(:state, 'awaiting_return') end @@ -172,7 +172,7 @@ describe CustomersWithBalance do before do order = create(:order, customer: customer, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, customer: customer, total: order_total, payment_total: payment_total) order.update_attribute(:state, 'returned') end diff --git a/spec/queries/outstanding_balance_spec.rb b/spec/queries/outstanding_balance_spec.rb index a662bcccf9..af880528f7 100644 --- a/spec/queries/outstanding_balance_spec.rb +++ b/spec/queries/outstanding_balance_spec.rb @@ -79,9 +79,9 @@ describe OutstandingBalance do context 'when no orders where paid' do before do order = create(:order, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') end it 'returns the customer balance' do @@ -95,9 +95,9 @@ describe OutstandingBalance do before do order = create(:order, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, total: order_total, payment_total: payment_total) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') end it 'returns the customer balance' do @@ -113,7 +113,7 @@ describe OutstandingBalance do before do create(:order, total: order_total, payment_total: order_total, state: 'canceled') order = create(:order, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') end it 'returns the customer balance' do @@ -127,7 +127,7 @@ describe OutstandingBalance do before do order = create(:order, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, total: order_total, payment_total: payment_total) order.update_attribute(:state, 'resumed') end @@ -143,7 +143,7 @@ describe OutstandingBalance do before do order = create(:order, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, total: order_total, payment_total: payment_total) order.update_attribute(:state, 'payment') end @@ -159,7 +159,7 @@ describe OutstandingBalance do before do order = create(:order, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') order = create(:order, total: order_total, payment_total: payment_total) order.update_attribute(:state, 'awaiting_return') end @@ -178,7 +178,7 @@ describe OutstandingBalance do order = create(:order, total: order_total, payment_total: payment_total) order.update_attribute(:state, 'returned') order = create(:order, total: order_total, payment_total: 0) - order.update_attribute(:state, 'checkout') + order.update_attribute(:state, 'complete') end it 'returns the customer balance' do