-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use in-memory objects in OrderUpdater and remove Adjustment callbacks #1389
Use in-memory objects in OrderUpdater and remove Adjustment callbacks #1389
Conversation
Currently we do this: - Add adjustment to item - Trigger `ItemAdjustments#update` via an after_create on Adjustment - Call `order.update!` on items order - Trigger `ItemAdjustments#update` via OrderUpdater#update_totals We should be able to eliminate the first call to `ItemAdjustments#update` because we can rely on `order.update!` being called. If it weren't called then order-level amounts would be incorrect.
3aa916f
to
929cb02
Compare
We now wait until OrderUpdater#update to update the in-memory items.
In real usage `order.update!` would be called after performing these actions (activating a promotion, adding tax).
Previously we did this: - Call ItemAdjustments#update on the in-memory line item - Call ItemAdjustments#update again, but *not* on the in-memory line item We've now eliminated the first call to ItemAdjustments#update, so the in-memory line item doesn't get updated.
order.cancellations.short_ship([inventory_unit_2]) | ||
line_item.reload |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and the change above it are unfortunate. :( See the commit message for details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an idea for getting OrderUpdater to operate on in-memory objects, to try to avoid problems like the above.
diff --git a/core/app/models/spree/order_updater.rb b/core/app/models/spree/order_updater.rb
index caff2a6..b21a70c 100644
--- a/core/app/models/spree/order_updater.rb
+++ b/core/app/models/spree/order_updater.rb
@@ -31,7 +31,15 @@ module Spree
end
def recalculate_adjustments
- all_adjustments.includes(:adjustable).map(&:adjustable).uniq.each { |adjustable| Spree::ItemAdjustments.new(adjustable).update }
+ adjustables = Set.new
+
+ # Prefer using in-memory objects when possible:
+ adjustables << order
+ adjustables.merge(order.line_items)
+ adjustables.merge(order.shipments)
+ # In case someone is attaching adjustments to something other than LineItems and Shipments:
+ adjustables.merge(all_adjustments.includes(:adjustable).map(&:adjustable))
+
+ adjustables.each do |adjustable|
+ Spree::ItemAdjustments.new(adjustable).update
+ end
end
Two risks I can think of:
- The in-memory objects may be stale. That might be an acceptable risk in the interest of performance.
- The above code will invoke
ItemAdjustments#update
on all shipments & line items, even if they don't currently have any adjustments on them. That will result in an extraselect * from spree_adjustments where adjustable_id = ?
for every line item/shipment that doesn't have at least one adjustment (extra compared to the existing code).
Any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really want to do this.
I don't think we should concern ourselves with someone attaching adjustments to something other than LineItems and Shipments, since the rest of the methods in OrderUpdater
won't take that into account.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jhawthorn great point. Here's a PR (against this PR) to do that: jordan-brough#7 Shall I merge that into this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall I merge that into this PR?
👍
I'm totally aware this might break some code as stores might not be expecting us to operate on the in-memory objects, but I'm 100% for the change. I think the order updater should always be operating on the in-memory objects and the more of these changes we make the faster and cleaner our updating process will be. |
My main concern with this is that the |
So that any already-loaded line item or shipment objects will be updated. To make it more likely that code like this: order.update! order.line_items.each { ... } will have the correct data without having to issue an `order.reload` after the `order.update!`.
@@ -56,13 +56,17 @@ module Spree | |||
end | |||
|
|||
it "update order adjustments" do | |||
# TODO: Make this into a more valid test, so that stubbing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually encouraged that this test is breaking. It's testing an obviously wrong thing (that the adjustment totals aren't updated despite being wrong). We could probably remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! I've gone ahead and made it simpler but more correct.
We shouldn't need to do this anymore since: - OrderUpdater now updates the in-memory LineItems and Shipments - This commit updates OrderContents#remove_from_line_item to remove destroyed line items from the order's cache.
Easier to understand what's wrong when it fails.
Don't need these reloads now that we operate on in-memory objects in OrderUpdater.
3169161
to
09c5f69
Compare
Previously the line_items association became stale in the order merger because items were being assigned to the order. By using line_items.<< we can keep the association up to date.
4720879
to
847a33d
Compare
@@ -155,7 +155,7 @@ | |||
inventory_unit_2.line_item.reload # UnitCancel#compute_amount needs updated amounts | |||
order.cancellations.short_ship([inventory_unit_2]) | |||
line_item.reload | |||
expect(line_item.adjustments.non_tax.sum(:amount)).to eq(-1.67) | |||
expect(line_item.adjustments.map(&:amount)).to match_array([0.01, -0.83, -0.84]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[0.01, -0.83, -0.84].inject(:+) #=> -1.66
and that 0.01 is the tax adjustment above 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
FYI I'm a bit worried that I've made the |
@@ -153,7 +152,7 @@ | |||
it "generates the correct total amount" do | |||
order.cancellations.short_ship([inventory_unit_1]) | |||
order.cancellations.short_ship([inventory_unit_2]) | |||
expect(line_item.adjustments.non_tax.sum(:amount)).to eq(-1.67) | |||
expect(line_item.adjustments.map(&:amount)).to match_array([0.01, -0.83, -0.84]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a little strange, as the three values now add up to -1.66. Is there a tax adjustment hidden there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Don't answer, Line 144-152 explain it all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mamhoff I went ahead and updated a bit because this spec was hard for me to follow when I first read it and @jhawthorn also left a comment similar to yours.
This is good. 👍 ! |
order.included_tax_total = all_items.sum(&:included_tax_total) | ||
order.additional_tax_total = all_items.sum(&:additional_tax_total) | ||
|
||
order.promo_total = all_items.sum(&:promo_total) + adjustments.promotion.eligible.sum(:amount) | ||
|
||
update_order_total | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's all pretty great! Thank you for it!
Just wanted to ask you on why you didn't go about to change adjustments.eligible
to an in-memory call as well? I think the collection has already been loaded by now somewhere in the ItemAdjustments
, and here you're calling a sum twice, so it should be a two SQL lookup save. A #promotion?
method already exists on Adjustment, only an #eligible?
one would need be added, which could be useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jhawthorn any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I really want to do this. I tried it and it caused a few errors, so I'd like to do that as a separate follow-up to this PR
Note: I found that the sporadic spec failures are specific to Postgres so I'm guessing it's something to do with DB result ordering but in any case at least I can repro the failures now. |
We are now triggering caching for shipments an line items when we call `order.contents.add`. This meant that `order.shipments` and `order.line_items` needed to be reloaded for these specs. Also, the `order.shipments.reload` that I had added caused flakiness because in Postgres they would sometimes reload in a random order and then `shipments.first` and `shipments.last` wouldn't be the expected items.
eb99086
to
0ca63c3
Compare
# Cause adjustable's total to be recalculated | ||
ItemAdjustments.new(adjustable).update | ||
end | ||
|
||
def require_promotion_code? | ||
promotion? && source.promotion.codes.any? | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wanted to point out this bad boy here:
It sort of relied on the "automatic" after destroy of adjustments. Should we maybe add an `order.update!' to all those incomplete orders, for which we have deleted an adjustment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking perhaps we should leave that for a separate PR? If it was relying on the after destroy then it was leaving the order in an inconsistent state anyway, because it didn't update the order-level totals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure; it was like that indeed 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a github issue for that: #1396
Great work on moving this forward everyone. Thank you! ❤️ |
My primary goal here is to make it easy to integrate 3rd party tax services
in a clean and reliable way by removing extraneous calls to
ItemAdjustments#update
. (See #1252.)Use in-memory objects in OrderUpdater:
Operate on
order.line_items
andorder.shipments
inside OrderUpdaterInstead of loading separate copies from the DB. This should give us:
order.update!
theorder.line_items
on that order object will get updated.The second point is extra important as we work on removing extraneous calls
to
ItemAdjustments
(see below), which previously had the side effect ofupdating some in-memory objects that won't otherwise get updated until
a
.reload
is invoked.Remove Adjustment callbacks:
Same idea as #1356, and likewise related to #1252.
Currently we do this:
ItemAdjustments#update
via an after_create on Adjustmentorder.update!
on items orderItemAdjustments#update
via OrderUpdater#update_totalsWe should be able to eliminate the first call to
ItemAdjustments#update
because we can rely onorder.update!
beingcalled. If it weren't called then order-level amounts would be
incorrect.