2

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?

Community
  • 1
  • 1
Zabba
  • 64,285
  • 47
  • 179
  • 207
  • 1
    NO. A global variable lives across requests and across machines. – PeterWong Feb 15 '11 at 14:35
  • What do you mean by global variable here? You would probably pass the user as a parameter to your methods. – Dan Rosenstark Feb 15 '11 at 14:36
  • @PeterWong, wow! Did not know that. But how can the global variable persist across machines? @Yar, I mean the `$someglobal`. – Zabba Feb 15 '11 at 14:46
  • rails in fact is a normal application ran on the server. As long as there is one application, each global variable is unique in that particular application. – PeterWong Feb 15 '11 at 14:50
  • that would be application-wide. Even session-wide would be too big a scope for what you want. Pass the user as a param. – Dan Rosenstark Feb 15 '11 at 14:52

5 Answers5

3

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
Rimian
  • 36,864
  • 16
  • 117
  • 117
Jordan
  • 1,230
  • 8
  • 8
1

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.

Andrew
  • 42,517
  • 51
  • 181
  • 281
  • Thanks for your answer. I could do business rules in the Car and other models, but I thought it better to have all logic in one place, since some models interact with each other quite closely. Also, why put `sell_car` in `User` as opposed to in `Car`? – Zabba Feb 15 '11 at 14:57
  • Put sell car in user because then you know who is doing the selling. From the controller you call `user.sell_car @myCar` or whatever. By calling it from the user you don't have to try to get session information (`current_user`) into your model, which is a bad idea. – Andrew Feb 15 '11 at 15:01
  • Like I said you certainly can make a Rules class if you want to, but I'm not sure that makes a lot of object-oriented sense. The Rails Way seems to be to ask the models questions about themselves. IE: User, sell a car. User, buy a car. Car, are you for sale? The logic in question is all VERY specific to the individual models, so why not implement it in the appropriate model and allow the models to interact naturally? Of course if you have insanely complicated transaction logic, then I understand why you might want to separate it to a `Transaction` model. – Andrew Feb 15 '11 at 15:04
0

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

VoronoiPotato
  • 3,113
  • 20
  • 30
0

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

bioneuralnet
  • 5,283
  • 1
  • 24
  • 30
0

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.

Josh Kovach
  • 7,679
  • 3
  • 45
  • 62