Unnest OrderManagement::Order declaration in two module declaration

This commit is contained in:
Luis Ramos
2020-07-10 17:31:13 +01:00
parent aed384183b
commit 8001e63f77
2 changed files with 408 additions and 404 deletions

View File

@@ -1,200 +1,202 @@
# frozen_string_literal: true
module OrderManagement::Order
class Updater
attr_reader :order
delegate :payments, :line_items, :adjustments, :shipments, :update_hooks, to: :order
module OrderManagement
module Order
class Updater
attr_reader :order
delegate :payments, :line_items, :adjustments, :shipments, :update_hooks, to: :order
def initialize(order)
@order = order
end
# This is a multi-purpose method for processing logic related to changes in the Order.
# It is meant to be called from various observers so that the Order is aware of changes
# that affect totals and other values stored in the Order.
#
# This method should never do anything to the Order that results in a save call on the
# object with callbacks (otherwise you will end up in an infinite recursion as the
# associations try to save and then in turn try to call +update!+ again.)
def update
update_totals
if order.completed?
update_payment_state
# give each of the shipments a chance to update themselves
shipments.each { |shipment| shipment.update!(order) }
update_shipment_state
def initialize(order)
@order = order
end
update_all_adjustments
# update totals a second time in case updated adjustments have an effect on the total
update_totals
# This is a multi-purpose method for processing logic related to changes in the Order.
# It is meant to be called from various observers so that the Order is aware of changes
# that affect totals and other values stored in the Order.
#
# This method should never do anything to the Order that results in a save call on the
# object with callbacks (otherwise you will end up in an infinite recursion as the
# associations try to save and then in turn try to call +update!+ again.)
def update
update_totals
order.update_attributes_without_callbacks(
{
payment_state: order.payment_state,
shipment_state: order.shipment_state,
item_total: order.item_total,
adjustment_total: order.adjustment_total,
payment_total: order.payment_total,
total: order.total
}
)
if order.completed?
update_payment_state
run_hooks
end
# give each of the shipments a chance to update themselves
shipments.each { |shipment| shipment.update!(order) }
update_shipment_state
end
def run_hooks
update_hooks.each { |hook| order.__send__(hook) }
end
update_all_adjustments
# update totals a second time in case updated adjustments have an effect on the total
update_totals
# Updates the following Order total values:
#
# - payment_total - the total value of all finalized Payments (excluding non-finalized Payments)
# - item_total - the total value of all LineItems
# - adjustment_total - the total value of all adjustments
# - total - the "order total". This is equivalent to item_total plus adjustment_total
def update_totals
order.payment_total = payments.completed.map(&:amount).sum
order.item_total = line_items.map(&:amount).sum
order.adjustment_total = adjustments.eligible.map(&:amount).sum
order.total = order.item_total + order.adjustment_total
end
order.update_attributes_without_callbacks(
{
payment_state: order.payment_state,
shipment_state: order.shipment_state,
item_total: order.item_total,
adjustment_total: order.adjustment_total,
payment_total: order.payment_total,
total: order.total
}
)
# Updates the +shipment_state+ attribute according to the following logic:
#
# - shipped - when all Shipments are in the "shipped" state
# - partial - when 1. at least one Shipment has a state of "shipped"
# and there is another Shipment with a state other than "shipped"
# or 2. there are InventoryUnits associated with the order that
# have a state of "sold" but are not associated with a Shipment
# - ready - when all Shipments are in the "ready" state
# - backorder - when there is backordered inventory associated with an order
# - pending - when all Shipments are in the "pending" state
#
# The +shipment_state+ value helps with reporting, etc. since it provides a quick and easy way
# to locate Orders needing attention.
def update_shipment_state
if order.backordered?
order.shipment_state = 'backorder'
else
# get all the shipment states for this order
shipment_states = shipments.states
if shipment_states.size > 1
# multiple shiment states means it's most likely partially shipped
order.shipment_state = 'partial'
run_hooks
end
def run_hooks
update_hooks.each { |hook| order.__send__(hook) }
end
# Updates the following Order total values:
#
# - payment_total - the total value of all finalized Payments (excluding non-finalized Payments)
# - item_total - the total value of all LineItems
# - adjustment_total - the total value of all adjustments
# - total - the "order total". This is equivalent to item_total plus adjustment_total
def update_totals
order.payment_total = payments.completed.map(&:amount).sum
order.item_total = line_items.map(&:amount).sum
order.adjustment_total = adjustments.eligible.map(&:amount).sum
order.total = order.item_total + order.adjustment_total
end
# Updates the +shipment_state+ attribute according to the following logic:
#
# - shipped - when all Shipments are in the "shipped" state
# - partial - when 1. at least one Shipment has a state of "shipped"
# and there is another Shipment with a state other than "shipped"
# or 2. there are InventoryUnits associated with the order that
# have a state of "sold" but are not associated with a Shipment
# - ready - when all Shipments are in the "ready" state
# - backorder - when there is backordered inventory associated with an order
# - pending - when all Shipments are in the "pending" state
#
# The +shipment_state+ value helps with reporting, etc. since it provides a quick and easy way
# to locate Orders needing attention.
def update_shipment_state
if order.backordered?
order.shipment_state = 'backorder'
else
# will return nil if no shipments are found
order.shipment_state = shipment_states.first
# TODO inventory unit states?
# if order.shipment_state && order.inventory_units.where(:shipment_id => nil).exists?
# shipments exist but there are unassigned inventory units
# order.shipment_state = 'partial'
# end
# get all the shipment states for this order
shipment_states = shipments.states
if shipment_states.size > 1
# multiple shiment states means it's most likely partially shipped
order.shipment_state = 'partial'
else
# will return nil if no shipments are found
order.shipment_state = shipment_states.first
# TODO inventory unit states?
# if order.shipment_state && order.inventory_units.where(:shipment_id => nil).exists?
# shipments exist but there are unassigned inventory units
# order.shipment_state = 'partial'
# end
end
end
order.state_changed('shipment')
end
# Updates the +payment_state+ attribute according to the following logic:
#
# - paid - when +payment_total+ is equal to +total+
# - balance_due - when +payment_total+ is less than +total+
# - credit_owed - when +payment_total+ is greater than +total+
# - failed - when most recent payment is in the failed state
#
# The +payment_state+ value helps with reporting, etc. since it provides a quick and easy way
# to locate Orders needing attention.
def update_payment_state
last_payment_state = order.payment_state
order.payment_state = infer_payment_state
track_payment_state_change(last_payment_state)
order.payment_state
end
def update_all_adjustments
order.adjustments.reload.each(&:update!)
end
def before_save_hook
shipping_address_from_distributor
end
# Sets the distributor's address as shipping address of the order for those
# shipments using a shipping method that doesn't require address, such us
# a pickup.
def shipping_address_from_distributor
return if order.shipping_method.blank? || order.shipping_method.require_ship_address
order.ship_address = order.address_from_distributor
end
private
def round_money(value)
(value * 100).round / 100.0
end
def infer_payment_state
if failed_payments?
'failed'
elsif canceled_and_not_paid_for?
'void'
else
infer_payment_state_from_balance
end
end
order.state_changed('shipment')
end
def infer_payment_state_from_balance
# This part added so that we don't need to override
# order.outstanding_balance
balance = order.outstanding_balance
balance = -1 * order.payment_total if canceled_and_paid_for?
# Updates the +payment_state+ attribute according to the following logic:
#
# - paid - when +payment_total+ is equal to +total+
# - balance_due - when +payment_total+ is less than +total+
# - credit_owed - when +payment_total+ is greater than +total+
# - failed - when most recent payment is in the failed state
#
# The +payment_state+ value helps with reporting, etc. since it provides a quick and easy way
# to locate Orders needing attention.
def update_payment_state
last_payment_state = order.payment_state
order.payment_state = infer_payment_state
track_payment_state_change(last_payment_state)
order.payment_state
end
def update_all_adjustments
order.adjustments.reload.each(&:update!)
end
def before_save_hook
shipping_address_from_distributor
end
# Sets the distributor's address as shipping address of the order for those
# shipments using a shipping method that doesn't require address, such us
# a pickup.
def shipping_address_from_distributor
return if order.shipping_method.blank? || order.shipping_method.require_ship_address
order.ship_address = order.address_from_distributor
end
private
def round_money(value)
(value * 100).round / 100.0
end
def infer_payment_state
if failed_payments?
'failed'
elsif canceled_and_not_paid_for?
'void'
else
infer_payment_state_from_balance
infer_state(balance)
end
end
def infer_payment_state_from_balance
# This part added so that we don't need to override
# order.outstanding_balance
balance = order.outstanding_balance
balance = -1 * order.payment_total if canceled_and_paid_for?
infer_state(balance)
end
def infer_state(balance)
if balance > 0
'balance_due'
elsif balance < 0
'credit_owed'
elsif balance.zero?
'paid'
def infer_state(balance)
if balance > 0
'balance_due'
elsif balance < 0
'credit_owed'
elsif balance.zero?
'paid'
end
end
end
# Tracks the state transition through a state_change for this order. It
# does so until the last state is reached. That is, when the infered next
# state is the same as the order has now.
#
# @param last_payment_state [String]
def track_payment_state_change(last_payment_state)
return if last_payment_state == order.payment_state
# Tracks the state transition through a state_change for this order. It
# does so until the last state is reached. That is, when the infered next
# state is the same as the order has now.
#
# @param last_payment_state [String]
def track_payment_state_change(last_payment_state)
return if last_payment_state == order.payment_state
order.state_changed('payment')
end
order.state_changed('payment')
end
# Taken from order.outstanding_balance in Spree 2.4
# See: https://github.com/spree/spree/commit/7b264acff7824f5b3dc6651c106631d8f30b147a
def canceled_and_paid_for?
order.canceled? && paid?
end
# Taken from order.outstanding_balance in Spree 2.4
# See: https://github.com/spree/spree/commit/7b264acff7824f5b3dc6651c106631d8f30b147a
def canceled_and_paid_for?
order.canceled? && paid?
end
def canceled_and_not_paid_for?
order.state == 'canceled' && order.payment_total.zero?
end
def canceled_and_not_paid_for?
order.state == 'canceled' && order.payment_total.zero?
end
def paid?
payments.present? && !payments.completed.empty?
end
def paid?
payments.present? && !payments.completed.empty?
end
def failed_payments?
payments.present? && payments.valid.empty?
def failed_payments?
payments.present? && payments.valid.empty?
end
end
end
end

View File

@@ -2,289 +2,291 @@
require 'spec_helper'
module OrderManagement::Order
describe Updater do
let(:order) { build(:order) }
let(:updater) { OrderManagement::Order::Updater.new(order) }
module OrderManagement
module Order
describe Updater do
let(:order) { build(:order) }
let(:updater) { OrderManagement::Order::Updater.new(order) }
before { allow(order).to receive(:backordered?) { false } }
before { allow(order).to receive(:backordered?) { false } }
it "updates totals" do
payments = [double(amount: 5), double(amount: 5)]
allow(order).to receive_message_chain(:payments, :completed).and_return(payments)
it "updates totals" do
payments = [double(amount: 5), double(amount: 5)]
allow(order).to receive_message_chain(:payments, :completed).and_return(payments)
line_items = [double(amount: 10), double(amount: 20)]
allow(order).to receive_messages line_items: line_items
line_items = [double(amount: 10), double(amount: 20)]
allow(order).to receive_messages line_items: line_items
adjustments = [double(amount: 10), double(amount: -20)]
allow(order).to receive_message_chain(:adjustments, :eligible).and_return(adjustments)
adjustments = [double(amount: 10), double(amount: -20)]
allow(order).to receive_message_chain(:adjustments, :eligible).and_return(adjustments)
updater.update_totals
expect(order.payment_total).to eq 10
expect(order.item_total).to eq 30
expect(order.adjustment_total).to eq(-10)
expect(order.total).to eq 20
end
context "updating shipment state" do
before do
allow(order).to receive_message_chain(:shipments, :shipped, :count).and_return(0)
allow(order).to receive_message_chain(:shipments, :ready, :count).and_return(0)
allow(order).to receive_message_chain(:shipments, :pending, :count).and_return(0)
updater.update_totals
expect(order.payment_total).to eq 10
expect(order.item_total).to eq 30
expect(order.adjustment_total).to eq(-10)
expect(order.total).to eq 20
end
it "is backordered" do
allow(order).to receive(:backordered?) { true }
updater.update_shipment_state
context "updating shipment state" do
before do
allow(order).to receive_message_chain(:shipments, :shipped, :count).and_return(0)
allow(order).to receive_message_chain(:shipments, :ready, :count).and_return(0)
allow(order).to receive_message_chain(:shipments, :pending, :count).and_return(0)
end
expect(order.shipment_state).to eq 'backorder'
end
it "is nil" do
allow(order).to receive_message_chain(:shipments, :states).and_return([])
allow(order).to receive_message_chain(:shipments, :count).and_return(0)
updater.update_shipment_state
expect(order.shipment_state).to be_nil
end
["shipped", "ready", "pending"].each do |state|
it "is #{state}" do
allow(order).to receive_message_chain(:shipments, :states).and_return([state])
it "is backordered" do
allow(order).to receive(:backordered?) { true }
updater.update_shipment_state
expect(order.shipment_state).to eq state.to_s
expect(order.shipment_state).to eq 'backorder'
end
it "is nil" do
allow(order).to receive_message_chain(:shipments, :states).and_return([])
allow(order).to receive_message_chain(:shipments, :count).and_return(0)
updater.update_shipment_state
expect(order.shipment_state).to be_nil
end
["shipped", "ready", "pending"].each do |state|
it "is #{state}" do
allow(order).to receive_message_chain(:shipments, :states).and_return([state])
updater.update_shipment_state
expect(order.shipment_state).to eq state.to_s
end
end
it "is partial" do
allow(order).to receive_message_chain(:shipments, :states).and_return(["pending", "ready"])
updater.update_shipment_state
expect(order.shipment_state).to eq 'partial'
end
end
it "is partial" do
allow(order).to receive_message_chain(:shipments, :states).and_return(["pending", "ready"])
updater.update_shipment_state
expect(order.shipment_state).to eq 'partial'
end
end
it "state change" do
order = create(:order)
order.shipment_state = 'shipped'
state_changes = double
allow(order).to receive(:state_changes) { state_changes }
expect(state_changes).to receive(:create).with(
previous_state: nil,
next_state: 'shipped',
name: 'shipment',
user_id: order.user_id
)
it "state change" do
order = create(:order)
order.shipment_state = 'shipped'
state_changes = double
allow(order).to receive(:state_changes) { state_changes }
expect(state_changes).to receive(:create).with(
previous_state: nil,
next_state: 'shipped',
name: 'shipment',
user_id: order.user_id
)
order.state_changed('shipment')
end
context "completed order" do
before { allow(order).to receive(:completed?) { true } }
it "updates payment state" do
expect(updater).to receive(:update_payment_state)
updater.update
order.state_changed('shipment')
end
it "updates shipment state" do
expect(updater).to receive(:update_shipment_state)
updater.update
end
context "completed order" do
before { allow(order).to receive(:completed?) { true } }
it "updates each shipment" do
shipment = build(:shipment)
shipments = [shipment]
allow(order).to receive_messages shipments: shipments
allow(shipments).to receive_messages states: []
allow(shipments).to receive_messages ready: []
allow(shipments).to receive_messages pending: []
allow(shipments).to receive_messages shipped: []
it "updates payment state" do
expect(updater).to receive(:update_payment_state)
updater.update
end
expect(shipment).to receive(:update!).with(order)
updater.update
end
end
it "updates shipment state" do
expect(updater).to receive(:update_shipment_state)
updater.update
end
context "incompleted order" do
before { allow(order).to receive_messages completed?: false }
it "updates each shipment" do
shipment = build(:shipment)
shipments = [shipment]
allow(order).to receive_messages shipments: shipments
allow(shipments).to receive_messages states: []
allow(shipments).to receive_messages ready: []
allow(shipments).to receive_messages pending: []
allow(shipments).to receive_messages shipped: []
it "doesnt update payment state" do
expect(updater).not_to receive(:update_payment_state)
updater.update
end
it "doesnt update shipment state" do
expect(updater).not_to receive(:update_shipment_state)
updater.update
end
it "doesnt update each shipment" do
shipment = build(:shipment)
shipments = [shipment]
allow(order).to receive_messages shipments: shipments
allow(shipments).to receive_messages states: []
allow(shipments).to receive_messages ready: []
allow(shipments).to receive_messages pending: []
allow(shipments).to receive_messages shipped: []
expect(shipment).not_to receive(:update!).with(order)
updater.update
end
end
it "updates totals twice" do
expect(updater).to receive(:update_totals).twice
updater.update
end
context "update adjustments" do
context "shipments" do
it "updates" do
expect(updater).to receive(:update_all_adjustments)
expect(shipment).to receive(:update!).with(order)
updater.update
end
end
end
it "is failed if no valid payments" do
allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(true)
context "incompleted order" do
before { allow(order).to receive_messages completed?: false }
updater.update_payment_state
expect(order.payment_state).to eq('failed')
end
it "doesnt update payment state" do
expect(updater).not_to receive(:update_payment_state)
updater.update
end
context "payment total is greater than order total" do
it "is credit_owed" do
order.payment_total = 2
order.total = 1
it "doesnt update shipment state" do
expect(updater).not_to receive(:update_shipment_state)
updater.update
end
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'credit_owed'
end
end
it "doesnt update each shipment" do
shipment = build(:shipment)
shipments = [shipment]
allow(order).to receive_messages shipments: shipments
allow(shipments).to receive_messages states: []
allow(shipments).to receive_messages ready: []
allow(shipments).to receive_messages pending: []
allow(shipments).to receive_messages shipped: []
context "order total is greater than payment total" do
it "is credit_owed" do
order.payment_total = 1
order.total = 2
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'balance_due'
end
end
context "order total equals payment total" do
it "is paid" do
order.payment_total = 30
order.total = 30
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'paid'
end
end
context "order is canceled" do
before do
order.state = 'canceled'
end
context "and is still unpaid" do
it "is void" do
order.payment_total = 0
order.total = 30
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'void'
expect(shipment).not_to receive(:update!).with(order)
updater.update
end
end
context "and is paid" do
it "updates totals twice" do
expect(updater).to receive(:update_totals).twice
updater.update
end
context "update adjustments" do
context "shipments" do
it "updates" do
expect(updater).to receive(:update_all_adjustments)
updater.update
end
end
end
it "is failed if no valid payments" do
allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(true)
updater.update_payment_state
expect(order.payment_state).to eq('failed')
end
context "payment total is greater than order total" do
it "is credit_owed" do
order.payment_total = 30
order.total = 30
allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(false)
allow(order).to receive_message_chain(:payments, :completed, :empty?).and_return(false)
order.payment_total = 2
order.total = 1
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'credit_owed'
end
end
context "and payment is refunded" do
it "is void" do
order.payment_total = 0
order.total = 30
allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(false)
allow(order).to receive_message_chain(:payments, :completed, :empty?).and_return(false)
context "order total is greater than payment total" do
it "is credit_owed" do
order.payment_total = 1
order.total = 2
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'void'
end
end
end
context 'when the set payment_state does not match the last payment_state' do
before { order.payment_state = 'previous_to_paid' }
context 'and the order is being updated' do
before { allow(order).to receive(:persisted?) { true } }
it 'creates a new state_change for the order' do
expect { updater.update_payment_state }
.to change { order.state_changes.size }.by(1)
}.to change { order.payment_state }.to 'balance_due'
end
end
context 'and the order is being created' do
before { allow(order).to receive(:persisted?) { false } }
context "order total equals payment total" do
it "is paid" do
order.payment_total = 30
order.total = 30
it 'creates a new state_change for the order' do
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'paid'
end
end
context "order is canceled" do
before do
order.state = 'canceled'
end
context "and is still unpaid" do
it "is void" do
order.payment_total = 0
order.total = 30
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'void'
end
end
context "and is paid" do
it "is credit_owed" do
order.payment_total = 30
order.total = 30
allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(false)
allow(order).to receive_message_chain(:payments, :completed, :empty?).and_return(false)
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'credit_owed'
end
end
context "and payment is refunded" do
it "is void" do
order.payment_total = 0
order.total = 30
allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(false)
allow(order).to receive_message_chain(:payments, :completed, :empty?).and_return(false)
expect {
updater.update_payment_state
}.to change { order.payment_state }.to 'void'
end
end
end
context 'when the set payment_state does not match the last payment_state' do
before { order.payment_state = 'previous_to_paid' }
context 'and the order is being updated' do
before { allow(order).to receive(:persisted?) { true } }
it 'creates a new state_change for the order' do
expect { updater.update_payment_state }
.to change { order.state_changes.size }.by(1)
end
end
context 'and the order is being created' do
before { allow(order).to receive(:persisted?) { false } }
it 'creates a new state_change for the order' do
expect { updater.update_payment_state }
.not_to change { order.state_changes.size }
end
end
end
context 'when the set payment_state matches the last payment_state' do
before { order.payment_state = 'paid' }
it 'does not create any state_change' do
expect { updater.update_payment_state }
.not_to change { order.state_changes.size }
end
end
end
context 'when the set payment_state matches the last payment_state' do
before { order.payment_state = 'paid' }
context '#before_save_hook' do
let(:distributor) { build(:distributor_enterprise) }
let(:shipment) { create(:shipment_with, :shipping_method, shipping_method: shipping_method) }
it 'does not create any state_change' do
expect { updater.update_payment_state }
.not_to change { order.state_changes.size }
end
end
context '#before_save_hook' do
let(:distributor) { build(:distributor_enterprise) }
let(:shipment) { create(:shipment_with, :shipping_method, shipping_method: shipping_method) }
before do
order.distributor = distributor
order.shipments = [shipment]
end
context 'when shipping method is pickup' do
let(:shipping_method) { create(:shipping_method_with, :pickup) }
let(:address) { build(:address, firstname: 'joe') }
before { distributor.address = address }
it "populates the shipping address from distributor" do
updater.before_save_hook
expect(order.ship_address.address1).to eq(distributor.address.address1)
before do
order.distributor = distributor
order.shipments = [shipment]
end
end
context 'when shipping_method is delivery' do
let(:shipping_method) { create(:shipping_method_with, :delivery) }
let(:address) { build(:address, firstname: 'will') }
before { order.ship_address = address }
context 'when shipping method is pickup' do
let(:shipping_method) { create(:shipping_method_with, :pickup) }
let(:address) { build(:address, firstname: 'joe') }
before { distributor.address = address }
it "does not populate the shipping address from distributor" do
updater.before_save_hook
expect(order.ship_address.firstname).to eq("will")
it "populates the shipping address from distributor" do
updater.before_save_hook
expect(order.ship_address.address1).to eq(distributor.address.address1)
end
end
context 'when shipping_method is delivery' do
let(:shipping_method) { create(:shipping_method_with, :delivery) }
let(:address) { build(:address, firstname: 'will') }
before { order.ship_address = address }
it "does not populate the shipping address from distributor" do
updater.before_save_hook
expect(order.ship_address.firstname).to eq("will")
end
end
end
end