ruby-on-railsdatabasemodeltrackingtemporal

Implementing rental store in Rails: how to track an inventory state over time?


Let's say you're implementing rails app for a snowboard rental store.

A given snowboard can be in one of 3 states:

  1. away for maintenance
  2. available at store X
  3. on loan to customer Y

The company needs to be able to view a rental history for

The rental history needs to include temporal data (e.g. Sally rented snowboard 0123 from Dec. 1, 2009 to Dec. 3 2009).

How would you design your model? Would you have a snowboard table with 4 columns (id, state, customer, store), and copy rows from this table, along with a timestamp, to a snowboard_history table every time the state changes?

Thanks!

(Note: I'm not actually trying to implement a rental store; this was just the simplest analogue I could think of.)


Solution

  • I would use a pair of plugins to get the job done. Which would use four models. Snowboard, Store, User and Audit.

    acts_as_state_machine and acts_as_audited

    AASM simplifies the state transitions. While auditing creates the history you want.

    The code for Store and User is trivial and acts_as_audited will handle the audits model.

    class Snowboard < ActiveRecord::Base
    
      include AASM
      belongs_to :store
      
    
      aasm_initial_state :unread
      acts_as_audited :only => :state
    
      aasm_state :maintenance
      aasm_state :available
      aasm_state :rented
    
      aasm_event :send_for_repairs do
        transitions :to => :maintenance, :from => [:available]
      end
    
      aasm_event :return_from_repairs do
        transitions :to => :available, :from => [:maintenance]
      end
    
      aasm_event :rent_to_customer do
       transitions :to => :rented, :from => [:available]
      end
    
      aasm_event :returned_by_customer do
        transitions :to => :available, :from => [:rented]
      end
    end
    
    class User < ActiveRecord::Base
      has_many :full_history, :class_name => 'Audit', :as => :user,
       :conditions => {:auditable_type => "Snowboard"}
    end    
    

    Assuming your customer is the current_user during the controller action when state changes that's all you need.

    To get a snowboard history:

    @snowboard.audits
    

    To get a customer's rental history:

    @customer.full_history
    

    You might want to create a helper method to shape a customer's history into something more useful. Maybe something like his:

     def rental_history
        history = []
        outstanding_rentals = {}
        full_history.each do |item|
          id = item.auditable_id
          if rented_at = outstanding_rentals.keys.delete(id)
            history << { 
              :snowboard_id => id, 
              :rental_start => rented_at,
              :rental_end => item.created_at
            }   
          else
            outstanding_rentals[:id] = item.created_at
          end
        end
        history << oustanding_rentals.collect{|key, value| {:snowboard_id => key,  
          :rental_start => value}
      end
    end