Skip to content

Commit

Permalink
Merge pull request #6643 from coopdevs/customer-balance-frontoffice
Browse files Browse the repository at this point in the history
Customer balance frontoffice
  • Loading branch information
sauloperez authored Jan 27, 2021
2 parents 7525620 + cc9e3fe commit c3897dd
Show file tree
Hide file tree
Showing 13 changed files with 471 additions and 86 deletions.
16 changes: 14 additions & 2 deletions app/controllers/spree/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ class UsersController < ::BaseController
before_action :set_locale
before_action :enable_embedded_shopfront

# Ignores invoice orders, only order where state: 'complete'
def show
@orders = @user.orders.where(state: 'complete').order('completed_at desc')
@orders = orders_collection

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

Expand Down Expand Up @@ -54,6 +58,14 @@ def update

private

def orders_collection
if OpenFoodNetwork::FeatureToggle.enabled?(:customer_balance, spree_current_user)
CompleteOrdersWithBalance.new(@user).query
else
@user.orders.where(state: 'complete').order('completed_at desc')
end
end

def load_object
@user ||= spree_current_user
if @user
Expand Down
5 changes: 5 additions & 0 deletions app/models/spree/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ def states
where("state != ?", state)
}

# All the states an order can be in after completing the checkout
FINALIZED_STATES = %w(complete canceled resumed awaiting_return returned).freeze

scope :finalized, -> { where(state: FINALIZED_STATES) }

def self.by_number(number)
where(number: number)
end
Expand Down
21 changes: 21 additions & 0 deletions app/queries/complete_orders_with_balance.rb
Original file line number Diff line number Diff line change
@@ -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_finalized_orders).query
end

private

def sorted_finalized_orders
@user.orders
.finalized
.select('spree_orders.*')
.order(completed_at: :desc)
end
end
39 changes: 39 additions & 0 deletions app/queries/customers_with_balance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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
end

def query
Customer.of(enterprise).
joins(left_join_complete_orders).
group("customers.id").
select("customers.*").
select(outstanding_balance_sum)
end

private

attr_reader :enterprise

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
# for a customer's balance.
def left_join_complete_orders
<<-SQL.strip_heredoc
LEFT JOIN spree_orders ON spree_orders.customer_id = customers.id
AND #{finalized_states.to_sql}
SQL
end

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
44 changes: 44 additions & 0 deletions app/queries/outstanding_balance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 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
# 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)
@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
ELSE 0 END
SQL
end

private

attr_reader :relation

def non_fulfilled_states_group
states = FINALIZED_NON_SUCCESSFUL_STATES.map { |state| Arel::Nodes.build_quoted(state) }
Arel::Nodes::Grouping.new(states)
end
end
8 changes: 8 additions & 0 deletions app/serializers/api/order_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ class OrderSerializer < ActiveModel::Serializer

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
Expand Down
61 changes: 0 additions & 61 deletions app/services/customers_with_balance.rb

This file was deleted.

5 changes: 3 additions & 2 deletions app/views/spree/users/show.html.haml
Original file line number Diff line number Diff line change
@@ -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}"))
Expand Down
16 changes: 16 additions & 0 deletions spec/controllers/spree/users_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@
# 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
Expand Down
61 changes: 61 additions & 0 deletions spec/queries/complete_orders_with_balance_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit c3897dd

Please sign in to comment.