&& order.has_refunds_owing?); raise HelpLetMeOutOfHereError Hi! I’m Rob Howard, and I’m here to talk to you about States, or Finite State Machines. It’s not as boring as it sounds. :-)
08:10:01 shipped_at NULL ... Cancelled. Probably. Created, then paid for, then cancelled. (We’re making assumptions about the order the above all happened in.)
can “Ship” an order, transitioning to “Shipped”. In this system, this is an Accepting (or Final) State. The Order cannot and will not transition to any other state from this one. (We indicate this with the double-circle.)
a CMS page in a mandatory-review system can’t go from “Draft” to “Published” without an intermediary “Reviewed” state, in this system we can’t go to “Shipped” from “Placed” without “Paid” in the middle.
Cancel Ship From “Placed”, we can also “Cancel”. Notice that we can transition to “Cancelled” from two states, “Placed” and “Paid”. This is important later.
of having the audit record being created in a model’s save() method (with conditional logic to check if it’s a state transition), we instead have the State Machine component of the model figure out when it needs to dump out a record.
:cancel, transition_to: :cancelled event :mark_as_paid, transition_to: :paid end state :paid do event :cancel, transition_to: :cancelled event :ship, transition_to: :shipped end state :cancelled state :shipped end def mark_as_paid halt! 'No valid payment found' if payment_owed? end def cancel halt! 'Purchaser still owed' if refunds_owed? end #... Here’s an example of a state machine library in use. It’s Ruby, but you don’t need to really understand it to get what’s going on. Note the list of states inside workflow do ... end.
:cancel, transition_to: :cancelled event :mark_as_paid, transition_to: :paid end state :paid do event :cancel, transition_to: :cancelled event :ship, transition_to: :shipped end state :cancelled state :shipped end def mark_as_paid halt! 'No valid payment found' if payment_owed? end def cancel halt! 'Purchaser still owed' if refunds_owed? end #... First, the “Placed” state, with two possible transitions: “cancel”, or “mark_as_paid”. It has a check (see def mark_as_paid) that prevents the transition from even happening if the conditions aren’t met.
:cancel, transition_to: :cancelled event :mark_as_paid, transition_to: :paid end state :paid do event :cancel, transition_to: :cancelled event :ship, transition_to: :shipped end state :cancelled state :shipped end def mark_as_paid halt! 'No valid payment found' if payment_owed? end def cancel halt! 'Purchaser still owed' if refunds_owed? end #... Same with “Paid”. We can’t “Cancel” without having refunded the customer everything they’re owed first.
:cancel, transition_to: :cancelled event :mark_as_paid, transition_to: :paid end state :paid do event :cancel, transition_to: :cancelled event :ship, transition_to: :shipped end state :cancelled state :shipped end def mark_as_paid halt! 'No valid payment found' if payment_owed? end def cancel halt! 'Purchaser still owed' if refunds_owed? end #... “Cancelled” and “Shipped” are our Accept/Final States. Neither have any way to transition back to other states. Let’s go back to the diagram.