Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8343681
Rename method that recalculates shipment state
forkata Nov 8, 2024
1664546
Rename method that recalculates payment state
forkata Nov 8, 2024
d455aab
Rename update_ private methods
AlistairNorman Nov 22, 2024
a859efc
Rename recalculate_ private methods
Noah-Silvera Nov 6, 2025
db760b0
Move require for DB query matchers to initializer
forkata Jul 4, 2025
caf4754
Copy existing `OrderUpdater` implementation
benjaminwil Oct 11, 2024
04cf743
Make OrderRecalculatorPatch prepend explicit
adammathys Jul 4, 2025
53a32ee
Add `persist` flag to `#recalculate`
benjaminwil Oct 11, 2024
849333d
Add describe block to Shipment#update_amounts test
Noah-Silvera Oct 25, 2024
784d3b5
Conditionally persist Shipment#update_amounts changes
Noah-Silvera Oct 25, 2024
59beeec
Prevent shipment updates from making DB writes
Noah-Silvera Oct 25, 2024
d6da25d
Reorder private methods
AlistairNorman Nov 22, 2024
f0e0cc3
Remove describe block for private method
AlistairNorman Nov 22, 2024
13d1c15
Test that changes to item totals are respected
adammathys Jan 30, 2025
07f5599
Pass persist flag to legacy promotion recalculator
adammathys Jan 31, 2025
65b5435
Pass persist to promotion.order_adjuster_class
sofiabesenski4 Dec 6, 2024
fde33aa
Support conditional persist in promotion chooser
AlistairNorman Feb 7, 2025
e43df3f
Add missing Promotions::OrderAdjuster spec
Noah-Silvera Feb 14, 2025
d9cad88
Don't persist line item on promotion application
Noah-Silvera Feb 14, 2025
bafd816
Test in-memory order updater in legacy promotions
sofiabesenski4 Mar 28, 2025
ebdd0ad
Add in-memory updater patch for legacy promotion system
Noah-Silvera May 15, 2025
29b60c6
Mark adjustments for destruction
forkata Mar 14, 2025
8aac5e6
Ignore dependent records marked for destruction
adammathys Jul 4, 2025
a8cd034
Allow the order updater to handle non-persisted line items
forkata May 22, 2025
f500efd
Create a manipulative query handler
senemsoy May 22, 2025
5f11b5d
Monitor manipulative queries in InMemoryOrderUpdater
sofiabesenski4 Jul 4, 2025
89c97da
Don't persist line item actions in compute_amount
AlistairNorman Jul 4, 2025
6c75072
Add tests to ensure compute amount doesn't persist
AlistairNorman Jul 4, 2025
ea9e25f
Add call stack to ManipulativeQueryMonitor warnings
sofiabesenski4 Jul 4, 2025
0485e88
Switch to attribute_assigns for item totals
adammathys Jul 4, 2025
070b81c
Test in-memory order updater with new promotions
adammathys Jul 4, 2025
6e2a452
Remove persist flag from update_shipment_amounts
kjriga Jul 4, 2025
1a0a387
Rework item total updating to not use persist flag
kjriga Jul 4, 2025
fbc1f96
Use `Shipment#recalculate_state`
adammathys Jul 4, 2025
79e04c6
Refactored shipment recalculation tests
benjaminwil Sep 11, 2025
e2e6ed2
Only log state changes when persisting new state
kjriga Sep 25, 2025
c736cd8
Update classic order updater specs
sofiabesenski4 Oct 9, 2025
ff11e8d
Make test assertion independent of config
forkata Oct 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ gem 'puma', '< 7', require: false
gem 'i18n-tasks', '~> 0.9', require: false
gem 'rspec_junit_formatter', require: false
gem 'yard', require: false
gem 'db-query-matchers', require: false

if ENV['GITHUB_ACTIONS']
gem "rspec-github", "~> 3.0", require: false
Expand Down
8 changes: 0 additions & 8 deletions admin/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,6 @@
require 'axe-rspec'
require 'axe-capybara'

# DB Query Matchers
require "db-query-matchers"
DBQueryMatchers.configure do |config|
config.ignores = [/SHOW TABLES LIKE/]
config.ignore_cached = true
config.schemaless = true
end

RSpec.configure do |config|
if ENV["GITHUB_ACTIONS"]
require "rspec/github"
Expand Down
262 changes: 262 additions & 0 deletions core/app/models/spree/in_memory_order_updater.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# frozen_string_literal: true

require 'spree/manipulative_query_monitor'

module Spree
class InMemoryOrderUpdater
attr_reader :order

# logs a warning when a manipulative query is made when the persist flag is set to false
class_attribute :log_manipulative_queries
self.log_manipulative_queries = true

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(persist: true)
monitor =
if log_manipulative_queries
Spree::ManipulativeQueryMonitor
else
proc { |&block| block.call }
end

order.transaction do
monitor.call do
recalculate_item_count
assign_shipment_amounts
end

if persist
update_totals(persist:)
else
monitor.call do
update_totals(persist:)
end
end

monitor.call do
if order.completed?
recalculate_payment_state
recalculate_shipment_state
end
end

Spree::Bus.publish(:order_recalculated, order:)

persist_totals if persist
end
end
alias_method :update, :recalculate
deprecate update: :recalculate, deprecator: Spree.deprecator

# Recalculates the state on all of them shipments, then recalculates 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 recalculate_shipment_state
shipments.each(&:recalculate_state)

order.shipment_state = determine_shipment_state
order.shipment_state
end
alias_method :update_shipment_state, :recalculate_shipment_state
deprecate update_shipment_state: :recalculate_shipment_state, deprecator: Spree.deprecator
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be extracted into a PR


# Recalculates 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 recalculate_payment_state
order.payment_state = determine_payment_state
order.payment_state
end
alias_method :update_payment_state, :recalculate_payment_state
deprecate update_payment_state: :recalculate_payment_state, deprecator: Spree.deprecator
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could also be extracted into PR


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 update_adjustments(persist:)
# 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(persist:)
update_tax_adjustments
assign_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(persist:)
recalculate_payment_total
recalculate_item_total
recalculate_shipment_total
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these should also be deprecated because even though they are private we all have evidence that this are overwritten in subclasses. can be extracted into a PR

update_adjustment_total(persist:)
end

def assign_shipment_amounts
shipments.each(&:assign_amounts)
end

def update_adjustment_total(persist:)
update_adjustments(persist:)

all_items = line_items + shipments
# Ignore any adjustments that have been marked for destruction in our
# calculations. They'll get removed when/if we persist the order.
valid_adjustments = adjustments.reject(&:marked_for_destruction?)
order_tax_adjustments = valid_adjustments.select(&:tax?)

order.adjustment_total = all_items.sum(&:adjustment_total) + valid_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)

recalculate_order_total
end

def update_promotions(persist:)
Spree::Config.promotions.order_adjuster_class.new(order).call(persist:)
end

def update_tax_adjustments
Spree::Config.tax_adjuster_class.new(order).adjust!
end

def update_cancellations
end
deprecate :update_cancellations, deprecator: Spree.deprecator

def assign_item_totals
[*line_items, *shipments].each do |item|
Spree::Config.item_total_class.new(item).recalculate!
end
end

def persist_item_totals
[*line_items, *shipments].each do |item|
next unless item.changed?

item.save! unless item.persisted?

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

def recalculate_payment_total
order.payment_total = payments.completed.includes(:refunds).sum { |payment| payment.amount - payment.refunds.sum(:amount) }
end

def recalculate_shipment_total
order.shipment_total = shipments.to_a.sum(&:cost)
recalculate_order_total
end

def recalculate_order_total
order.total = order.item_total + order.shipment_total + order.adjustment_total
end

def recalculate_item_count
order.item_count = line_items.to_a.sum(&:quantity)
end

def recalculate_item_total
order.item_total = line_items.to_a.sum(&:amount)
recalculate_order_total
end

def persist_totals
shipments.each(&:persist_amounts)
persist_item_totals
log_state_change("payment")
log_state_change("shipment")
order.save!
end

def log_state_change(name)
state = "#{name}_state"
previous_state, current_state = order.changes[state]

if previous_state != current_state
# Enqueue the job to track this state change
StateChangeTrackingJob.perform_later(
order,
previous_state,
current_state,
Time.current,
name
)
end
end
end
end
2 changes: 1 addition & 1 deletion core/app/models/spree/null_promotion_adjuster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def initialize(order)
@order = order
end

def call
def call(persist: true) # rubocop:disable Lint/UnusedMethodArgument
@order
end
end
Expand Down
8 changes: 4 additions & 4 deletions core/app/models/spree/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ def valid_credit_cards

def fulfill!
shipments.each { |shipment| shipment.update_state if shipment.persisted? }
recalculator.update_shipment_state
recalculator.recalculate_shipment_state
save!
end

Expand Down Expand Up @@ -758,13 +758,13 @@ def finalize
all_adjustments.each(&:finalize!)

# update payment and shipment(s) states, and save
recalculator.update_payment_state
recalculator.recalculate_payment_state
shipments.each do |shipment|
shipment.update_state
shipment.finalize!
end

recalculator.update_shipment_state
recalculator.recalculate_shipment_state
save!

touch :completed_at
Expand Down Expand Up @@ -804,7 +804,7 @@ def ensure_inventory_units
end

def ensure_promotions_eligible
Spree::Config.promotions.order_adjuster_class.new(self).call
Spree::Config.promotions.order_adjuster_class.new(self).call(persist: false)

if promo_total_changed?
restart_checkout_flow
Expand Down
Loading
Loading