Skip to content
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

Merged

Conversation

jordan-brough
Copy link
Contributor

@jordan-brough jordan-brough commented Aug 16, 2016

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 and order.shipments inside OrderUpdater
Instead of loading separate copies from the DB. This should give us:

  • Better performance
  • Return more accurate results. I.e. when calling order.update! the
    order.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 of
updating 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:

  • 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.

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.
@jordan-brough jordan-brough force-pushed the remove-adjustment-after-callbacks branch from 3aa916f to 929cb02 Compare August 16, 2016 15:36
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
Copy link
Contributor Author

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.

Copy link
Contributor Author

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:

  1. The in-memory objects may be stale. That might be an acceptable risk in the interest of performance.
  2. 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 extra select * 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?

Copy link
Contributor

@jhawthorn jhawthorn Aug 16, 2016

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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?

👍

@cbrunsdon
Copy link
Contributor

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.

@jhawthorn
Copy link
Contributor

My main concern with this is that the after_destroy is currently necessary. I'm worried that when destroying the last adjustment on and item, an order.update! won't successfully update the item's total since it won't be included in all_adjustments.map(&:adjustable). The change to recalculate_adjustments should fix that, but we should probably make a failing test to be sure.

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
Copy link
Contributor

@jhawthorn jhawthorn Aug 16, 2016

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.

Copy link
Contributor Author

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.

jordan-brough and others added 4 commits August 16, 2016 14:28
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.
@jordan-brough jordan-brough force-pushed the remove-adjustment-after-callbacks branch from 3169161 to 09c5f69 Compare August 16, 2016 20:33
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.
@jordan-brough jordan-brough force-pushed the remove-adjustment-after-callbacks branch from 4720879 to 847a33d Compare August 16, 2016 21:17
@@ -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])
Copy link
Contributor

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 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

@jordan-brough jordan-brough changed the title Remove Adjustment callbacks for update_adjustable_adjustment_total Use in-memory objects in OrderUpdater and remove Adjustment callbacks Aug 17, 2016
@jordan-brough
Copy link
Contributor Author

FYI I'm a bit worried that I've made the spec/features/admin/orders/order_details_spec.rb flaky somehow. I haven't been able to repro these failures for that spec locally:
https://circleci.com/gh/solidusio/solidus/4156
https://circleci.com/gh/solidusio/solidus/4161
https://circleci.com/gh/solidusio/solidus/4151

@@ -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])
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

@jordan-brough jordan-brough Aug 17, 2016

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.

@mamhoff
Copy link
Contributor

mamhoff commented Aug 17, 2016

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
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jhawthorn any thoughts?

Copy link
Contributor

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

@jordan-brough
Copy link
Contributor Author

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.
@jordan-brough jordan-brough force-pushed the remove-adjustment-after-callbacks branch from eb99086 to 0ca63c3 Compare August 18, 2016 19:48
# Cause adjustable's total to be recalculated
ItemAdjustments.new(adjustable).update
end

def require_promotion_code?
promotion? && source.promotion.codes.any?
end
Copy link
Contributor

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:

https://github.com/solidusio/solidus/blob/master/core/app/models/concerns/spree/adjustment_source.rb#L7

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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 👍

Copy link
Contributor Author

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

@jhawthorn
Copy link
Contributor

:shipit:

@jordan-brough jordan-brough merged commit 7b9559d into solidusio:master Aug 18, 2016
@jordan-brough jordan-brough deleted the remove-adjustment-after-callbacks branch August 18, 2016 20:10
@bbuchalter
Copy link
Contributor

Great work on moving this forward everyone. Thank you! ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants