-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Copy existing
OrderUpdater
implementation
In subsequent commits we'll ensure that this can update orders in memory, without persisting changes using manipulative DB queries.
- Loading branch information
1 parent
fc9dab2
commit 58c16d7
Showing
2 changed files
with
618 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
# frozen_string_literal: true | ||
|
||
module Spree | ||
class InMemoryOrderUpdater | ||
attr_reader :order | ||
delegate :payments, :line_items, :adjustments, :all_adjustments, :shipments, :quantity, 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 recalculate | ||
order.transaction do | ||
update_item_count | ||
update_shipment_amounts | ||
update_totals | ||
if order.completed? | ||
update_payment_state | ||
update_shipments | ||
update_shipment_state | ||
end | ||
Spree::Bus.publish :order_recalculated, order: order | ||
persist_totals | ||
end | ||
end | ||
alias_method :update, :recalculate | ||
deprecate update: :recalculate, deprecator: Spree.deprecator | ||
|
||
# Updates the +shipment_state+ attribute according to the following logic: | ||
# | ||
# shipped when all Shipments are in the "shipped" state | ||
# partial when at least one Shipment has a state of "shipped" and there is another Shipment with a state other than "shipped" | ||
# or 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 | ||
log_state_change('shipment') do | ||
order.shipment_state = determine_shipment_state | ||
end | ||
|
||
order.shipment_state | ||
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 | ||
# void when the order has been canceled and the payment total is 0 | ||
# | ||
# 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 | ||
log_state_change('payment') do | ||
order.payment_state = determine_payment_state | ||
end | ||
|
||
order.payment_state | ||
end | ||
|
||
private | ||
|
||
def determine_payment_state | ||
if payments.present? && payments.valid.empty? && order.outstanding_balance != 0 | ||
'failed' | ||
elsif order.state == 'canceled' && order.payment_total.zero? | ||
'void' | ||
elsif order.outstanding_balance > 0 | ||
'balance_due' | ||
elsif order.outstanding_balance < 0 | ||
'credit_owed' | ||
else | ||
# outstanding_balance == 0 | ||
'paid' | ||
end | ||
end | ||
|
||
def determine_shipment_state | ||
if order.backordered? | ||
'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 | ||
'partial' | ||
else | ||
# will return nil if no shipments are found | ||
shipment_states.first | ||
end | ||
end | ||
end | ||
|
||
# This will update and select the best promotion adjustment, update tax | ||
# adjustments, update cancellation adjustments, and then update the total | ||
# fields (promo_total, included_tax_total, additional_tax_total, and | ||
# adjustment_total) on the item. | ||
# @return [void] | ||
def recalculate_adjustments | ||
# Promotion adjustments must be applied first, then tax adjustments. | ||
# This fits the criteria for VAT tax as outlined here: | ||
# http://www.hmrc.gov.uk/vat/managing/charging/discounts-etc.htm#1 | ||
# It also fits the criteria for sales tax as outlined here: | ||
# http://www.boe.ca.gov/formspubs/pub113/ | ||
update_promotions | ||
update_taxes | ||
update_item_totals | ||
end | ||
|
||
# Updates the following Order total values: | ||
# | ||
# +payment_total+ The total value of all finalized Payments (NOTE: non-finalized Payments are excluded) | ||
# +item_total+ The total value of all LineItems | ||
# +adjustment_total+ The total value of all adjustments (promotions, credits, etc.) | ||
# +promo_total+ The total value of all promotion adjustments | ||
# +total+ The so-called "order total." This is equivalent to +item_total+ plus +adjustment_total+. | ||
def update_totals | ||
update_payment_total | ||
update_item_total | ||
update_shipment_total | ||
update_adjustment_total | ||
end | ||
|
||
def update_shipment_amounts | ||
shipments.each(&:update_amounts) | ||
end | ||
|
||
# give each of the shipments a chance to update themselves | ||
def update_shipments | ||
shipments.each(&:update_state) | ||
end | ||
|
||
def update_payment_total | ||
order.payment_total = payments.completed.includes(:refunds).sum { |payment| payment.amount - payment.refunds.sum(:amount) } | ||
end | ||
|
||
def update_shipment_total | ||
order.shipment_total = shipments.to_a.sum(&:cost) | ||
update_order_total | ||
end | ||
|
||
def update_order_total | ||
order.total = order.item_total + order.shipment_total + order.adjustment_total | ||
end | ||
|
||
def update_adjustment_total | ||
recalculate_adjustments | ||
|
||
all_items = line_items + shipments | ||
order_tax_adjustments = adjustments.select(&:tax?) | ||
|
||
order.adjustment_total = all_items.sum(&:adjustment_total) + adjustments.sum(&:amount) | ||
order.included_tax_total = all_items.sum(&:included_tax_total) + order_tax_adjustments.select(&:included?).sum(&:amount) | ||
order.additional_tax_total = all_items.sum(&:additional_tax_total) + order_tax_adjustments.reject(&:included?).sum(&:amount) | ||
|
||
update_order_total | ||
end | ||
|
||
def update_item_count | ||
order.item_count = line_items.to_a.sum(&:quantity) | ||
end | ||
|
||
def update_item_total | ||
order.item_total = line_items.to_a.sum(&:amount) | ||
update_order_total | ||
end | ||
|
||
def persist_totals | ||
order.save! | ||
end | ||
|
||
def log_state_change(name) | ||
state = "#{name}_state" | ||
old_state = order.public_send(state) | ||
yield | ||
new_state = order.public_send(state) | ||
if old_state != new_state | ||
order.state_changes.new( | ||
previous_state: old_state, | ||
next_state: new_state, | ||
name: name, | ||
user_id: order.user_id | ||
) | ||
end | ||
end | ||
|
||
def update_promotions | ||
Spree::Config.promotions.order_adjuster_class.new(order).call | ||
end | ||
|
||
def update_taxes | ||
Spree::Config.tax_adjuster_class.new(order).adjust! | ||
|
||
[*line_items, *shipments].each do |item| | ||
tax_adjustments = item.adjustments.select(&:tax?) | ||
# Tax adjustments come in not one but *two* exciting flavours: | ||
# Included & additional | ||
|
||
# Included tax adjustments are those which are included in the price. | ||
# These ones should not affect the eventual total price. | ||
# | ||
# Additional tax adjustments are the opposite, affecting the final total. | ||
item.included_tax_total = tax_adjustments.select(&:included?).sum(&:amount) | ||
item.additional_tax_total = tax_adjustments.reject(&:included?).sum(&:amount) | ||
end | ||
end | ||
|
||
def update_cancellations | ||
end | ||
deprecate :update_cancellations, deprecator: Spree.deprecator | ||
|
||
def update_item_totals | ||
[*line_items, *shipments].each do |item| | ||
# The cancellation_total isn't persisted anywhere but is included in | ||
# the adjustment_total | ||
item.adjustment_total = item.adjustments. | ||
reject(&:included?). | ||
sum(&:amount) | ||
|
||
if item.changed? | ||
item.update_columns( | ||
promo_total: item.promo_total, | ||
included_tax_total: item.included_tax_total, | ||
additional_tax_total: item.additional_tax_total, | ||
adjustment_total: item.adjustment_total, | ||
updated_at: Time.current, | ||
) | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.