-
-
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
Remove state machine gem from Spree::Shipment #2656
Conversation
|
||
context 'when state is ready' do | ||
before { shipment.state = 'ready' } | ||
it { is_expected.to be true} |
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.
Space missing inside }.
10177b8
to
0b44a29
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.
I love the explicitness of this change and think this is the right thing to do.
Have some small change requests and the recommendation to move shared functionality of state changes into a module.
But otherwise very nice work 👌
core/app/models/spree/shipment.rb
Outdated
!requires_shipment? | ||
end | ||
|
||
def can_transition_from_pending_to_ready? | ||
Spree::Deprecation.warn \ |
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.
There is a nice little class method from ActiveSupport
def can_transition_from_pending_to_ready?
...
end
deprecate can_transition_from_pending_to_ready?: :inventory_can_ship?, deprecator: Spree::Deprecation
could we use that instead?
core/app/models/spree/shipment.rb
Outdated
!requires_shipment? | ||
end | ||
|
||
def can_transition_from_pending_to_ready? | ||
Spree::Deprecation.warn \ | ||
'Spree::Shipment#can_transition_from_pending_to_ready? is deprecated. ' \ | ||
'Use Spree::Shipment#inventory_can_ship? instead.' | ||
order.can_ship? && | ||
inventory_units.all? { |iu| iu.shipped? || iu.allow_ship? || iu.canceled? } && | ||
(order.paid? || !Spree::Config[:require_payment_to_ship]) |
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.
could we call inventory_can_ship?
in here?
core/app/models/spree/shipment.rb
Outdated
@@ -14,6 +21,10 @@ class Shipment < Spree::Base | |||
has_many :state_changes, as: :stateful | |||
has_many :cartons, -> { uniq }, through: :inventory_units | |||
|
|||
validates :state, presence: true, inclusion: { in: [READY, PENDING, SHIPPED, CANCELED] } |
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.
presence: true
is superfluous if we also check the inclusion in something that's not having nil
.
Maybe we could also add default value for the state
column?
core/app/models/spree/shipment.rb
Outdated
@@ -14,6 +21,10 @@ class Shipment < Spree::Base | |||
has_many :state_changes, as: :stateful | |||
has_many :cartons, -> { uniq }, through: :inventory_units | |||
|
|||
validates :state, presence: true, inclusion: { in: [READY, PENDING, SHIPPED, CANCELED] } | |||
|
|||
after_initialize :set_default_state |
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.
We could have a default value for the state
column instead.
core/app/models/spree/shipment.rb
Outdated
update_attributes( | ||
state: new_state, | ||
updated_at: Time.current | ||
) |
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.
ActiveRecord already sets updated_at
for us.
Also, the bang at the end of the method name indicates that this method raises if the record couldn't be saved. We should use update!
here.
update!(state: new_state)
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.
We should consider to move the methods change_state!
and store_state_change
into a module we share with all models that have state
changes.
core/app/models/spree/shipment.rb
Outdated
def can_transition_from_pending_to_shipped? | ||
Spree::Deprecation.warn \ | ||
'Spree::Shipment#can_transition_from_pending_to_shipped is deprecated. ' \ | ||
'It is equivalent to the negation of Spree::Shipment#requires_shipment?' |
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.
dito
core/app/models/spree/shipment.rb
Outdated
end | ||
|
||
def pend! | ||
pend || (raise InvalidStateChange) |
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.
weird usage of parenthesis here.
Either
raise InvalidStateChange
or
raise(InvalidStateChange)
core/app/models/spree/shipment.rb
Outdated
end | ||
|
||
def ready! | ||
ready || (raise InvalidStateChange) |
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.
Ditto, weird usage of parenthesis
core/app/models/spree/shipment.rb
Outdated
end | ||
|
||
def ship! | ||
ship || (raise InvalidStateChange) |
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.
Ditto, parenthesis
core/app/models/spree/shipment.rb
Outdated
end | ||
end | ||
|
||
def set_default_state |
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.
A database default would be preferable
0b44a29
to
0a9a56c
Compare
core/app/models/spree/shipment.rb
Outdated
@@ -14,6 +21,10 @@ class Shipment < Spree::Base | |||
has_many :state_changes, as: :stateful | |||
has_many :cartons, -> { uniq }, through: :inventory_units | |||
|
|||
validates :state, inclusion: { in: [READY, PENDING, SHIPPED, CANCELED] } | |||
|
|||
#after_initialize :set_default_state |
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.
Layout/LeadingCommentSpace: Missing space after #.
0a9a56c
to
f0cc82b
Compare
@tvdeyen Thanks for all the great feedback! I've gone ahead and made most of the requested changes. I haven't pulled out the With all that being said, I'd love to hear any insight you may have on a better way to pull out the shared functionality! Lastly, in regards to things like Thanks again for taking time to review my work |
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.
Thanks @jgayfer this makes total sense to me.
I still have questions about using ActiveModel::Dirty
, but this is not mandatory as this still works without, but maybe worth trying.
Thanks for your work on this 👏
|
||
class AddDefaultStateToShipment < ActiveRecord::Migration[5.1] | ||
def change | ||
change_column_default(:spree_shipments, :state, Spree::Shipment::PENDING) |
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.
We usually don't want to rely on code inside the migrations. Although very unlikely that the value for Spree::Shipment::PENDING
will change ever, it still is more reliable to use the value here instead.
|
||
def change_state!(new_state) | ||
previous_state = state | ||
return if new_state == previous_state |
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.
We may can make use of ActiveModel::Dirty
here?
previous_state = state | ||
change_state!(SHIPPED) | ||
after_ship | ||
after_resume if previous_state == CANCELED |
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.
Can we try to make use of ActiveModel::Dirty
here?
I'm not agree with remove the I feel that reimplementing Like you mention maybe Finally if you want to remove |
f0cc82b
to
515288c
Compare
|
||
it { is_expected.to include carton } | ||
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.
Layout/TrailingWhitespace: Trailing whitespace detected.
436f8e8
to
dade297
Compare
@tvdeyen I made the change regarding the database migration (which definitely makes sense). I will make this change to my other PRs early next week. Regarding |
@jgayfer yes. Like I said not mandatory, but still worth trying, as we are duplicating AR behavior here. |
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.
Would you please extract this state
validation into a method? Thanks
core/app/models/spree/shipment.rb
Outdated
@@ -14,6 +21,8 @@ class Shipment < Spree::Base | |||
has_many :state_changes, as: :stateful | |||
has_many :cartons, -> { distinct }, through: :inventory_units | |||
|
|||
validates :state, inclusion: { in: [READY, PENDING, SHIPPED, CANCELED] } |
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.
We should extract this into a method, as stores might want to extend/change states.
There are a few reasons for removing this reference. First, we should be explicit about which values we want to test; reaching into the state machine adds an extra layer of abstraction that reduces the readability of the test. Second, by calling `Spree::Shipment.state_machine`, we are actually instantiating a state machine on the `Spree::Shipment` class, which should only ever happen within the `Shipment` class itself.
dade297
to
8398bba
Compare
@tvdeyen Changes have been made. I wasn't entirely sure exactly how someone might want to go about changing what states are valid, so I tried to keep the options open. There is a new constant |
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 wasn't entirely sure exactly how someone might want to go about changing what states are valid, so I tried to keep the options open.
@jgayfer lots of stores add states to core models. Additional shipment states, payment states, etc.
Please let's not encourage people to overwrite constants as extension point. That's why I think we should rename this constant.
core/app/models/spree/shipment.rb
Outdated
@@ -14,6 +22,8 @@ class Shipment < Spree::Base | |||
has_many :state_changes, as: :stateful | |||
has_many :cartons, -> { distinct }, through: :inventory_units | |||
|
|||
validate :validate_state |
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.
Can we call this method valid_state
or is_valid_state
? Reads better.
core/app/models/spree/shipment.rb
Outdated
PENDING = 'pending' | ||
SHIPPED = 'shipped' | ||
CANCELED = 'canceled' | ||
VALID_STATES = [READY, PENDING, SHIPPED, CANCELED] |
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.
Can we rename this constant to DEFAULT_STATES
? We don't want to give the impression that overwriting a constant is a good extension point (because it's not)
The state machine gem is something that we don't want around long term, as it has been known to cause headaches. The state machine in `Spree::Shipment` is fairly straight forward, which makes it a good candidate to try and remove. By removing the state machine gem, the logic for states is now entirely contained within `Spree::Shipment`. This helps the readability of the code and also makes it easier to override behaviour that was previously buried within the state machine gem.
These transition methods were used by the state machine gem. Semantically, these methods made sense in that context. However, with the removal of the state machine gem, these methods no longer read very well. `inventory_can_ship?` is a new method introduced in this commit, which is just a renaming of `can_transition_from_pending_to_ready?`. The two other transition methods (`can_transition_from_canceled_to_ready?` and `can_transition_from_pending_to_shipped?`) can be replaced with a call to `!requires_shipment?`. The end result is code that is much easier to parse as the the method names are much more telling of what is actually happening.
These methods were used by the state machine gem, and no longer make much semantic sense. The same functionality can be found in `!requires_shipment?` and `inventory_can_ship?`.
8398bba
to
8cea551
Compare
@tvdeyen Changes have been made 👍 |
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.
Thanks 👌
reopened in #3038 |
The state_machines gem has been used throughout Solidus since the earliest days of Spree. We use it to track the "state" our shipments/payments/orders are in, as well as plug custom functionality into the transitions between those states (
before_transition
/after_transition
).However, our use of intermingled state machines between Payments, Shipments and Orders causes problems for both new and established developers. The state machine transitions are difficult to understand and debug, the order the transitions are executed in can be tough to control, and properly controlling database transactions during a transition is very poorly understood.
We believe the downsides of implementing our state machines using the state_machines gem to be a net negative for the project.
This PR changes the
Spree::Shipment
class, explicitly defining the important methods that would have been generated from the state_machines metaprogramming. The advantages of doing this are improved code clarity, debug-ability, as well as making it more easy to completely override methods regarding state.The first two commits of this pull request remove the
state_machine
fromSpree::Shipment
without changing the external API of the model. The last two commits go a bit further and deprecate the transition methods that were used by the state machine in an attempt to improve code readability. If we don't want to go ahead and deprecate the transition methods, the first two commits can just be taken on their own, keeping the external API ofSpree::Shipment
intact.While we could re-implement the
before_transition
/after_transition
calls on theSpree::Shipment
model, we believe the following examples would be a better way to support customising functionality before or after a transition (withship
used as an example).