Skip to content

Commit 9190b94

Browse files
benjaminwiladammathysstewartHarmony Evangelinajarednorman
committed
Copy existing OrderUpdater implementation
In subsequent commits we'll ensure that this can update orders in memory, without persisting changes using manipulative DB queries. Co-authored-by: Adam Mueller <adam@super.gd> Co-authored-by: Andrew Stewart <andrew@super.gd> Co-authored-by: Harmony Evangelina <harmony@super.gd> Co-authored-by: Jared Norman <jared@super.gd> Co-authored-by: Kendra Riga <kendra@super.gd> Co-authored-by: Nick Van Doorn <nick@super.gd> Co-authored-by: Noah Silvera <noah@super.gd> Co-authored-by: Senem Soy <senem@super.gd> Co-authored-by: Sofia Besenski <sofia@super.gd> Co-authored-by: Tom Van Manen <tom@super.gd>
1 parent a452590 commit 9190b94

File tree

2 files changed

+618
-0
lines changed

2 files changed

+618
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
# frozen_string_literal: true
2+
3+
module Spree
4+
class InMemoryOrderUpdater
5+
attr_reader :order
6+
delegate :payments, :line_items, :adjustments, :all_adjustments, :shipments, :quantity, to: :order
7+
8+
def initialize(order)
9+
@order = order
10+
end
11+
12+
# This is a multi-purpose method for processing logic related to changes in the Order.
13+
# It is meant to be called from various observers so that the Order is aware of changes
14+
# that affect totals and other values stored in the Order.
15+
#
16+
# This method should never do anything to the Order that results in a save call on the
17+
# object with callbacks (otherwise you will end up in an infinite recursion as the
18+
# associations try to save and then in turn try to call +update!+ again.)
19+
def recalculate
20+
order.transaction do
21+
update_item_count
22+
update_shipment_amounts
23+
update_totals
24+
if order.completed?
25+
update_payment_state
26+
update_shipments
27+
update_shipment_state
28+
end
29+
Spree::Bus.publish :order_recalculated, order: order
30+
persist_totals
31+
end
32+
end
33+
alias_method :update, :recalculate
34+
deprecate update: :recalculate, deprecator: Spree.deprecator
35+
36+
# Updates the +shipment_state+ attribute according to the following logic:
37+
#
38+
# shipped when all Shipments are in the "shipped" state
39+
# partial when at least one Shipment has a state of "shipped" and there is another Shipment with a state other than "shipped"
40+
# or there are InventoryUnits associated with the order that have a state of "sold" but are not associated with a Shipment.
41+
# ready when all Shipments are in the "ready" state
42+
# backorder when there is backordered inventory associated with an order
43+
# pending when all Shipments are in the "pending" state
44+
#
45+
# The +shipment_state+ value helps with reporting, etc. since it provides a quick and easy way to locate Orders needing attention.
46+
def update_shipment_state
47+
log_state_change('shipment') do
48+
order.shipment_state = determine_shipment_state
49+
end
50+
51+
order.shipment_state
52+
end
53+
54+
# Updates the +payment_state+ attribute according to the following logic:
55+
#
56+
# paid when +payment_total+ is equal to +total+
57+
# balance_due when +payment_total+ is less than +total+
58+
# credit_owed when +payment_total+ is greater than +total+
59+
# failed when most recent payment is in the failed state
60+
# void when the order has been canceled and the payment total is 0
61+
#
62+
# The +payment_state+ value helps with reporting, etc. since it provides a quick and easy way to locate Orders needing attention.
63+
def update_payment_state
64+
log_state_change('payment') do
65+
order.payment_state = determine_payment_state
66+
end
67+
68+
order.payment_state
69+
end
70+
71+
private
72+
73+
def determine_payment_state
74+
if payments.present? && payments.valid.empty? && order.outstanding_balance != 0
75+
'failed'
76+
elsif order.state == 'canceled' && order.payment_total.zero?
77+
'void'
78+
elsif order.outstanding_balance > 0
79+
'balance_due'
80+
elsif order.outstanding_balance < 0
81+
'credit_owed'
82+
else
83+
# outstanding_balance == 0
84+
'paid'
85+
end
86+
end
87+
88+
def determine_shipment_state
89+
if order.backordered?
90+
'backorder'
91+
else
92+
# get all the shipment states for this order
93+
shipment_states = shipments.states
94+
if shipment_states.size > 1
95+
# multiple shiment states means it's most likely partially shipped
96+
'partial'
97+
else
98+
# will return nil if no shipments are found
99+
shipment_states.first
100+
end
101+
end
102+
end
103+
104+
# This will update and select the best promotion adjustment, update tax
105+
# adjustments, update cancellation adjustments, and then update the total
106+
# fields (promo_total, included_tax_total, additional_tax_total, and
107+
# adjustment_total) on the item.
108+
# @return [void]
109+
def recalculate_adjustments
110+
# Promotion adjustments must be applied first, then tax adjustments.
111+
# This fits the criteria for VAT tax as outlined here:
112+
# http://www.hmrc.gov.uk/vat/managing/charging/discounts-etc.htm#1
113+
# It also fits the criteria for sales tax as outlined here:
114+
# http://www.boe.ca.gov/formspubs/pub113/
115+
update_promotions
116+
update_taxes
117+
update_item_totals
118+
end
119+
120+
# Updates the following Order total values:
121+
#
122+
# +payment_total+ The total value of all finalized Payments (NOTE: non-finalized Payments are excluded)
123+
# +item_total+ The total value of all LineItems
124+
# +adjustment_total+ The total value of all adjustments (promotions, credits, etc.)
125+
# +promo_total+ The total value of all promotion adjustments
126+
# +total+ The so-called "order total." This is equivalent to +item_total+ plus +adjustment_total+.
127+
def update_totals
128+
update_payment_total
129+
update_item_total
130+
update_shipment_total
131+
update_adjustment_total
132+
end
133+
134+
def update_shipment_amounts
135+
shipments.each(&:update_amounts)
136+
end
137+
138+
# give each of the shipments a chance to update themselves
139+
def update_shipments
140+
shipments.each(&:update_state)
141+
end
142+
143+
def update_payment_total
144+
order.payment_total = payments.completed.includes(:refunds).sum { |payment| payment.amount - payment.refunds.sum(:amount) }
145+
end
146+
147+
def update_shipment_total
148+
order.shipment_total = shipments.to_a.sum(&:cost)
149+
update_order_total
150+
end
151+
152+
def update_order_total
153+
order.total = order.item_total + order.shipment_total + order.adjustment_total
154+
end
155+
156+
def update_adjustment_total
157+
recalculate_adjustments
158+
159+
all_items = line_items + shipments
160+
order_tax_adjustments = adjustments.select(&:tax?)
161+
162+
order.adjustment_total = all_items.sum(&:adjustment_total) + adjustments.sum(&:amount)
163+
order.included_tax_total = all_items.sum(&:included_tax_total) + order_tax_adjustments.select(&:included?).sum(&:amount)
164+
order.additional_tax_total = all_items.sum(&:additional_tax_total) + order_tax_adjustments.reject(&:included?).sum(&:amount)
165+
166+
update_order_total
167+
end
168+
169+
def update_item_count
170+
order.item_count = line_items.to_a.sum(&:quantity)
171+
end
172+
173+
def update_item_total
174+
order.item_total = line_items.to_a.sum(&:amount)
175+
update_order_total
176+
end
177+
178+
def persist_totals
179+
order.save!
180+
end
181+
182+
def log_state_change(name)
183+
state = "#{name}_state"
184+
old_state = order.public_send(state)
185+
yield
186+
new_state = order.public_send(state)
187+
if old_state != new_state
188+
order.state_changes.new(
189+
previous_state: old_state,
190+
next_state: new_state,
191+
name: name,
192+
user_id: order.user_id
193+
)
194+
end
195+
end
196+
197+
def update_promotions
198+
Spree::Config.promotions.order_adjuster_class.new(order).call
199+
end
200+
201+
def update_taxes
202+
Spree::Config.tax_adjuster_class.new(order).adjust!
203+
204+
[*line_items, *shipments].each do |item|
205+
tax_adjustments = item.adjustments.select(&:tax?)
206+
# Tax adjustments come in not one but *two* exciting flavours:
207+
# Included & additional
208+
209+
# Included tax adjustments are those which are included in the price.
210+
# These ones should not affect the eventual total price.
211+
#
212+
# Additional tax adjustments are the opposite, affecting the final total.
213+
item.included_tax_total = tax_adjustments.select(&:included?).sum(&:amount)
214+
item.additional_tax_total = tax_adjustments.reject(&:included?).sum(&:amount)
215+
end
216+
end
217+
218+
def update_cancellations
219+
end
220+
deprecate :update_cancellations, deprecator: Spree.deprecator
221+
222+
def update_item_totals
223+
[*line_items, *shipments].each do |item|
224+
# The cancellation_total isn't persisted anywhere but is included in
225+
# the adjustment_total
226+
item.adjustment_total = item.adjustments.
227+
reject(&:included?).
228+
sum(&:amount)
229+
230+
if item.changed?
231+
item.update_columns(
232+
promo_total: item.promo_total,
233+
included_tax_total: item.included_tax_total,
234+
additional_tax_total: item.additional_tax_total,
235+
adjustment_total: item.adjustment_total,
236+
updated_at: Time.current,
237+
)
238+
end
239+
end
240+
end
241+
end
242+
end

0 commit comments

Comments
 (0)