How to implement "business rules" in Rails?

1.9k views Asked by At

What is the way to implement "business rules" in Rails?

Let us say I have a car and want to sell it:

car = Cars.find(24)
car.sell

car.sell method will check a few things:

does current_user own the car?
    check: car.user_id == current_user.id
is the car listed for sale in the sales catalog?
    check: car.catalogs.ids.include? car.id
    
if all o.k. then car is marked as sold.

I was thinking of creating a class called Rules:

class Rules
    def initialize(user,car)
        @user = user
        @car = car
    end

    def can_sell_car?
        @car.user_id == @user.id && @car.catalogs.ids.include? @car.id
    end
end

And using it like this:

def Car
    def sell
        if Rules.new(current_user,self).can_sell_car
            ..sell the car...
        else
            @error_message = "Cannot sell this car"
            nil
        end
    end
end

As for getting the current_user, I was thinking of storing it in a global variable? I think that whenever a controller action is called, it's always a "fresh" call right? If so then storing the current user as a global variable should not introduce any risks..(like some other user being able to access another user's details)

Any insights are appreciated!

UPDATE

So, the global variable route is out! Thanks to PeterWong for pointing out that global variables persist!

I've now thinking of using this way:

class Rules
    def self.can_sell_car?(current_user, car)
       ......checks....
    end
end

And then calling Rules.can_sell_car?(current_user,@car) from the controller action. Any thoughts on this new way?

5

There are 5 answers

0
VoronoiPotato On

I would think this would a prime candidate for using a database, and then you could use Ruby to query the different tables.

3
Andrew On

First, the standard rails practice is to keep all business logic in the models, not the controllers. It looks like you're heading that direction, so that's good -- BUT: be aware, there isn't a good clean way to get to the current_user from the model.

I wouldn't make a new Rules model (although you can if you really want to do it that way), I would just involve the user model and the car. So, for instance:

class User < ActiveRecord::Base
...
  def sell_car( car )
    if( car.user_id == self.id && car.for_sale? )
      # sell car
    end
  end
...
end

class Car < ActiveRecord::Base
...
  def for_sale?
    !catalog_id.nil?
  end
...
end

Obviously I'm making assumptions about how your Catalog works, but if cars that are for_sale belong_to a catalog, then that method would work - otherwise just adjust the method as necessary to check if the car is listed in a catalog or not. Honestly it would probably be a good idea to set a boolean value on the Car model itself, this way users could simply toggle the car being for sale or not for sale whenever you want them to ( either by marking the car for sale, or by adding the car to a catalog, etc. ).

I hope this gives you some direction! Please feel free to ask questions.


EDIT: Another way to do this would be to have methods in your models like:

user.buy_car( car )
car.transfer_to( user )

There are many ways to do it putting the logic in the object its interacting with.

0
Jordan On

I'd use the following tables:

For buyers and sellers:

people(id:int,name:string)

class Person << ActiveRecord::Base
  has_many :cars, :as => :owner
  has_many :sales, :as => :seller, :class_name => 'Transfer'
  has_many :purchases, :as => :buyer, :class_name => 'Transfer'
end

cars(id:int,owner_id:int, vin:string, year:int,make:string,model:string,listed_at:datetime)

listed_at is the flag to see if a Car is for sale or not

class Car << ActiveRecord::Base
  belongs_to :owner, :class_name => 'Person'
  has_many :transfers

  def for_sale?
    not listed_at.nil?
  end
end

transfers(id:int,car_id:int,seller_id:int,buyer_id:int)

class Transfer << ActiveRecord::Base
  belongs_to :car
  belongs_to :seller, :class_name => 'Person'
  belongs_to :buyer, :class_name => 'Person'

  validates_with Transfer::Validator

  def car_owned_by_seller?
     seller_id == car.owner_id
  end
end

Then you can use this custom validator to setup your rules.

class Transfer::Validator << ActiveModel::Validator
  def validate(transfer)
     transfer.errors[:base] = "Seller doesn't own car" unless transfer.car_owned_by_seller?
     transfer.errors[:base] = "Car isn't for sale" unless transfer.car.for_sale?
  end
end
0
Josh Kovach On

I'm doing something similar with users and what they can do with photo galleries. I'm using devise for users and authentication, and then I set up several methods in the user model that determine if the user has various permissions (users have many galleries through permissions) to act on that gallery. I think it looks like the biggest problem you are having is with determining your current user, which can be handled quite easily with Devise, and then you can add a method to the user model and check current_user.can_sell? to authorized a sale.

0
bioneuralnet On

You might take a look at the declarative authorization gem - https://github.com/stffn/declarative_authorization

While it's pre-configured for CRUD actions, you can easily add your own actions (buy, sell) and put their business logic in the authorization_rules.rb config file. Then, in your controllers, views, and even models!, you can easily ask permitted_to? :buy, @car