The state_machine gem is a great tool for putting business rules into your Ruby applications. But sometimes rules have exceptions. How do you handle those?
In this post, I'll show you how my team has used state_machine together with Authority to allow certain people to make exceptions to the rules.
Starting with state_machine
Let's look at a normal use case for state_machine.
For instance, suppose you have a Rails app where employees process loan applications. The flow looks something like this:
Created +-> Submitted +-> Reviewed +-> Verified +-> Approved
+ +
| |
| v
+------> Denied
An loan application can move smoothly from Created to Approved, or it can be denied during the Review or Verify stages. It can't be moved from Approved to Reviewed willy-nilly; that's just not valid.
That's how the business logic is described to you, and you implement it.
When the state_machine breaks down
But soon managers are coming to you, asking for help.
- "One of my employees mistakenly denied this. Would you approve it for me?"
- "This one says it's verified, but it wasn't reviewed thoroughly. Can you push it back two steps?"
Having a developer manually edit the database field is a waste of everyone's time.
The issue here is that the business rules aren't quite correct. In reality, an application can only move through the process like this unless a manager says otherwise. Managers have special "override power" to move an application back from Verified to Submitted, from Denied to Accepted, etc. Your code just doesn't reflect that fact yet.
The Manual Override state
You can solve this problem by adding a Manual Override state. The rule is that anything can transition to Manual Override, and it can transition to anything. Only managers can move to or from Manual Override.
Yes, I know. "Yuck!", you say. Your state machine diagram just got a lot more complex. This isn't a perfect solution, but it has a couple of advantages:
- You still have a state machine, so you still have some control. For instance, you could say that some states really are final and Manual Override isn't possible.
- Anything you've been doing with your transitions, such as recording who did it, can still apply. This gives an audit trail for management's actions, too.
Controlling Manual Override
So how do you control who can use Manual Override? Here's an approach we've used.
First, we use a feature of state_machine called "implicit transitions". Rather than calling a method to change states (for example, application.verify
), we have a form with a radio button where the user chooses the next state; for example, they may choose between Verified and Denied. That sets a property like account.status_state_event = :verify
; the transition will happen on save. This lets us have some other rules, like "you can't change state at all without entering a note explaining why".
Second, we define a method on Account called using_manual_override?
.
class Account < ActiveRecord::Base
# Are we moving to/from manual override?
def using_manual_override?
status_state_event == :manually_override || status_state == 'manual_override'
end
end
Third, we define the authorizer to disallow Manual Override updates unless the user is a manager.
class AccountAuthorizer < Authority::Authorizer
def updatable_by?(user, options = {})
if options[:manual_override] || resource.using_manual_override?
user.manager?
else
true
end
end
end
Fourth, we define a helper to get events that the current user is allowed to trigger.
module AccountsHelper
def available_events_for_account(account)
account.status_state_events.tap do |events|
unless current_user.can_update?(account, manual_override: true)
events.delete(:manually_override)
end
end
end
end
Finally, we build the form options.
%ul
- status_state_events = available_events_for_account(@account)
- if status_state_events.any?
- status_state_events.map(&:to_s).sort.each do |event|
%li
= link_to '#', data: {toggle: 'radio'} do
= f.radio_button :status_state_event, event
= event.titleize
- else
%li
.no-results No Actions Available.
Account is #{@account.status.name}.
The end result is that when managers look at the form, they see "Manually Override" as an option, but workers just see things like "Approve" and "Deny".
Conclusion
Like I said, this isn't a perfect solution. If your state machine is already complex, having a manual override state can make the graph of possible transitions explode in size.
On the positive side, since everything still goes through the state machine, you still have some control.
- You can assert that some states aren't eligible for Manual Override
-
You can have uniform recording of who changed what with
after_transition any => any, do: :my_tracking_method
I hope you find this technique useful, and if you're not already using Authority or state_machine, be sure to check them out!