-
-
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
Change order state to merged
instead of destroying it when merging orders
#3486
base: main
Are you sure you want to change the base?
Change order state to merged
instead of destroying it when merging orders
#3486
Conversation
9cff561
to
6bace8f
Compare
4bd51f5
to
8fbe989
Compare
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 great, debugging merged orders is like trying to catch ghosts 👻
I left a couple suggestions/considerations.
class AddMergedToOrderId < ActiveRecord::Migration[5.2] | ||
def change | ||
add_column :spree_orders, :merged_to_order_id, :integer, limit: 4 | ||
add_foreign_key :spree_orders, :spree_orders, column: :merged_to_order_id |
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 would also consider adding a merged_at
timestamp, especially when reconstructing weird edge cases that can be a precious information. In alternative the default order merger should at least add a note mentioning the date of the merge.
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.
@elia I think that would be redundant. We have state_changes
on orders, and since merged
is a new state, a new record pops up after merging the order:
2.6.3 :001 > order2.state_changes
=> [#<Spree::StateChange:0x00007fea97cd4f90
id: 1,
name: "order",
previous_state: "cart",
stateful_id: 2,
user_id: 2,
stateful_type: "Spree::Order",
next_state: "merged",
created_at: Fri, 07 Feb 2020 11:28:08 UTC +00:00,
updated_at: Fri, 07 Feb 2020 11:28:08 UTC +00:00>]
core/app/models/spree/order.rb
Outdated
@@ -54,6 +54,9 @@ class CannotRebuildShipments < StandardError; end | |||
deprecate temporary_credit_card: :temporary_payment_source, deprecator: Spree::Deprecation | |||
deprecate :temporary_credit_card= => :temporary_payment_source=, deprecator: Spree::Deprecation | |||
|
|||
has_one :from_merged_order, foreign_key: :merged_to_order_id, class_name: 'Spree::Order', inverse_of: :merged_to_order |
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 recall that a single orders can be the result of merging multiple orders. Should this be a has_many
?
solidus/core/lib/spree/core/controller_helpers/order.rb
Lines 60 to 66 in 9f99aa6
def set_current_order | |
if try_spree_current_user && current_order | |
try_spree_current_user.orders.by_store(current_store).incomplete.where('id != ?', current_order.id).each do |order| | |
current_order.merge!(order, try_spree_current_user) | |
end | |
end | |
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.
@elia thank you for pointing this out, I did not realize this could happen as well.
87c5cc4
to
ff17b85
Compare
When merging two orders together, one of them (the one passed as argument to `Spree::Order#merge!`) will now transition to the `merged` state. When an order is in `merged` state, it cannot transition to any other state. In order for an order to become merged, we need to set the new Active Record relation `#merged_to_order`. The association allows to keep a reference, for mostly for debugging purposes, to what order the merged order was merged to. The order that survives from the merge process has the corresponding inverse relation `#from_merged_order` that points to the merged order. One thing to keep in mind is that the order state machine should not define the `#merge!` method, as it's already defined for the `Spree::Order` class, see `Spree::Order#merge!(order)`. This is not actually an issue, as the existing `#merge!` method manages the state transition by leveraging the non-persisting method `#merge` that is implicitly defined by the state machine. This little inconsistency allows to keep the existing interface, without needing to deprecate the existing `Spree::Order#merge!` interface and adding conditional checks during the transitional time on the method arity (the state machine version of `#merge!` will ignore any passed params).
Instead of destroying the merged order, the class `Spree::OrderMerger` now transition the order to the `merged` state.
Merged orders are basically dead orders, they don't have any meaningful information (line items are moved to the surviving order of the merge process ) so there's not much value in displaying them on the backend.
ff17b85
to
9905650
Compare
@@ -179,6 +182,14 @@ def self.not_canceled | |||
where.not(state: 'canceled') | |||
end | |||
|
|||
def self.merged |
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.
Is there a reason these aren't scopes?
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.
Only for consistency with the ::cancelled
and ::not_canceled
class methods defined just above.
We've come across this as well, but we have opted not to add more stuff to the order state machine (which IMO does way too many things already). What we did was create a |
@mamhoff thanks for the feedback. I think you have a point about the state machine, though I don't see adding this new state as a big concern... still this solution goes in the opposite direction of simplifying the SM (BTW, I like your PR!). I think one big plus for the state machine is that the merged order is locked down, so it will not transition to other states. IMHO this cannot be achieved in a similarly straight forward way by using other approaches. But more in general, all the possible solutions to this issue have a problem in common: there are (irrelevant?) orders that are not destroyed anymore, and DB queries will start picking them up, unless stores actively remove them in their custom code. So, stopping to destroy these orders looks to me like a significant breaking change. I'm wondering if, instead of providing a new default approach to the problem, we should just offer to stores a few alternative merging strategies, built-in in Solidus, like this one (and yours) and let them chose their preference. But I'm not sure that a full-featured solution can be limited to just setting one preference, for example the merge strategy class, as different solutions will also need different ways to exclude merged orders from DB queries... maybe an extension would be a more suitable place 🤔 |
@kennyadsl has there been made any decision on this? I find merging orders a compelling feature but would like some guidance as it's not something we would extensively invest into. |
I'm ok with this change, but this is a big one and potentially very problematic in terms of backward compatibility, so I guess it need to go in the next major release (upon @solidusio/core-team consensus). |
I think there should be a way doing it without backwards compatibility issues as it is a new state. I find disappearing orders very concerning as I imagine that there are actually companies connecting solidus directly to xero or other fiscal instruments. Any way to keep a tap on this? |
@fthobe the non-breaking way of merging this would be to put it behind a configuration flag that's not active by default. That would be great although not always trivial. In any case someone needs to update the PR and rebase on the latest version, so if anyone is willing to take up both task (rebasing and adding the configuration flag) I would be absolutely in favor 👍. |
A question came up: |
Wait. Can we please step back a little bit and talk about the use case of this? A bit of background on the usual user flow will be helpful I guess. So, a yet anonymous user comes to the store and puts something in to the cart. A new order record gets created. This is an unusual, but powerful feature of Solidus. Even the cart is already an order record. The user then either creates a new account or logs into an existing account. Either way the anonymous order (the cart from current session) gets merged with all incomplete orders (due to the fact this happens every time the user logs in it's usually one incomplete order). And the previous anonymous and now merged order gets destroyed, because it does not serve any purpose anymore. The cart has now been transferred into a users order. If we now keep the anonymous order it will produce a lot of garbage in the database. What is the use case of keeping them? This behavior needs to be at least behind a feature flag or even in a separate order state machine. And users need to be informed about this behavior. But in the first place I would love to hear about the benefits. |
Honestly, pure data collection and profiling.
Half of today's recommendation engines are either based on sessions or abandoned / concluded carts. Actually I would go many steps further:
Throw it in data studio or tableau and you have some fascinating metrics to display. There's a whole order life cycle and long term abandonment data story to tell here and I wouldn't want to drop any of it. |
All stores I work on use third party tools for that. Even if we would use the state machine for that we would need to track the order the anonymous cart has been merged in. If there is a need for that, you can implement it in your store or create an extension. The order merger class and the state machine are configurable classes and easily adopted to specific needs that way. |
I can see your point why this can be done in my store, but I still struggle for any number of reasons with destroying any data without proper documentation. I feel taking up the decision to destroy data is massive compared to cost of storage. A fully agree to keep a flag, a separate state machine I see as drastically complicating things (might be wrong though). Can we take a look at the current PR and see if it's salvageable (later this year) and put it behind a setting. |
If I recall correctly, the main driver for this change was that it's very hard to debug issues on orders that are merged and destroyed, so suddenly disappear from the database while still being present in logs prior to the merge. |
@kennyadsl you're right, that was the original driver for the PR. |
What spaghetti Said. |
@tvdeyen A stupid question: Why is it not possible to Carry over the Order, why do we need to destroy it in the First Place? |
I was not involved or even around when this was introduced into Spree decades ago. My guess is, because it was considered as not useful information, regarding that this is a stale anonymous cart after it has been merged. Without keeping track of where it has been merged into it does not make sense to keep, I guess. It leads to false data, because the cart content has been merged over to a new order and we do not know who it belongs to. It could give a false impression of having lots of abandoned carts, although they have been converted into a payed order later on. |
Funny enough this might be a thing that is not breaking if we just update the user of an order instead of destroying it, or am I wrong? |
@spaghetticode if I recall correctly we also had a case in which some payments were attached to the then destroyed order, that alone to me would be a good reason to reverse the default in the next major. Customization can still be implemented either to remove the new state or to add other behaviors.
No, I think this is triggered when you have an anonymous cart and you login to a customer account that also have a current cart, and you want to merge the items from both. |
Seems like only bad options here. @tvdeyen can you live with hiding it behind a setting? |
@elia that's quite likely - the merged orders issue emerged multiple times while working on that particular store. |
Do you have some examples: I mean this applies only to uncompleted orders. |
@fthobe where exactly the payment happens can vary and I'm sure some older payment methods tried to mark an order as complete after the payment was successful. I think something else at that point caused the order to reset to the initial state and then it got merged. It's admittedly an edge case but we really wished to have a better paper trail at the time. |
This seems to be a bug rather than anything else. Only incomplete orders get merged. I would like to not keep garbage just because there might be potential bugs in customers implementations. There are plenty of options (like paper_trail) to keep track of records changes. |
It would not be something we touch before June / July. Do we want to leave this open or is it definitively something that has no chance on merging? |
@tvdeyen it was due to how the state machine handles payments rather than a customer implementation bug, and the old concept that an order is complete only after a payment is attached to it. We are not cleaning up abandoned carts by default, which is something that was needed in every single store I worked on eventually. In that case it's easy to add a recurring cleanup job that drops merged orders older than some date. Do you see any negative impact besides having additional order data around? |
No, if we make sure that the orders are clearly marked as what they are I have no objections. An unobtrusive way could be to introduce a |
Yes, that would be the absolute dream. Can we settle on that and gms lists this for the July tasklist? |
I think that was pretty much what was made in this PR It was pointed out by @elia that merged could come from multiple orders.
Has lot of potential! |
Description
This is an (opinionated, see later) attempt at #1449.
Destroying orders when merging them is not ideal, as we lose the ability to debug what happened, which may be very important in some edge cases.
This implementation adds the
merged
state toSpree::Order
and updates theSpree::OrderMerger
class accordingly. The merged order is associated with the resulting order via the#merged_to_order
association.The opinionated part is I kept the existing
Spree::Order#merge!
functionality and interface rather than switching to the one provided by default by the state machine when adding the merge event, see https://github.com/solidusio/solidus/compare/master...nebulab:spaghetticode/order-merged-state?expand=1#diff-2c3e70899d4f22d0569021b01b0e307fR62 (though the auto-generatedSpree::Order#merge
method is retained and used inSpree::OrderMerge
).Doing things differently would have required more extensive code changes, deprecations, and other tradeoffs.
Checklist: