Warning: here comes a small novel.
Part 1: setting up the associations
I'd recommend reading the Rails guide on associations thoroughly, bookmark it, and read it again, because this is a key thing to understand properly, and can be a bit tricky - there are lots of options once you go beyond basic associations.
One thing to notice about your app is that your users have two roles, buyers and sellers. You're going to need to be careful with the names of your associations - Does @user.offers
return the offers the user has made, or the offers the user has received? You might want to be able to put lists of both these things in the user's profile.
The basic relationships you're describing are fairly simple:
A user can sell many products, so User has_many :products
and Product belongs_to :user
A user can make many offers, so User has_many :offers
and Offer belongs_to :user
A product may receive many offers so Product has_many :offers
and Offer belongs_to :product
That's all well and good, and you could certainly get by just doing this - in which case you can skip down to Part 2 :)
However, as soon as you start trying to add the through
relationships the waters are going to get muddy. After all,
Offer belongs_to :user
(the buyer), but it also has a user through product (the seller)
User has_many :products
(that they are selling), but they also have many products through offers (that they are buying - well, trying to buy).
Aargh, confusing!
This is the point when you need the :class_name
option, which lets you name an association differently to the class it refers to, and the :source
option, which lets you name associations on the 'from' model differently to the 'through' model.
So you might then form your associations like this:
# User
has_many :products_selling, class_name: 'Product'
has_many :offers_received, class_name: 'Offer',
through: :products_selling, source: :offers
has_many :offers_made, class_name: 'Offer'
has_many :products_buying, class_name: 'Product',
through: :offers_made, source: :product
# Product
belongs_to :seller, class_name: 'User', foreign_key: :user_id
has_many :offers
has_many :buyers, class_name: 'User', through: :offers
# Offer
belongs_to :product
belongs_to :buyer, class_name: 'User', foreign_key: :user_id
has_one :seller, class_name: 'User', through: :product
Although if you renamed your user_id
columns to seller_id
in the products
table, and buyer_id
in the offers
table, you wouldn't need those :foreign_key
options.
Part 2: accepting/rejecting offers
There's a number of ways to tackle this. I would put a boolean field accepted
on Offer
and then you could have something like
# Offer
def accept
self.accepted = true
save
end
def reject
self.accepted = false
save
end
and you could find the outstanding offers (where accepted
is null)
scope :outstanding, where(accepted: nil)
To get the accept/reject logic happening in the controller, you might consider adding new RESTful actions (the linked guide is another one worth reading thoroughly!). You should find a line like
resources :offers
in config/routes.rb, which provides the standard actions index
, show
, edit
, etc. You can change it to
resources :offers do
member do
post :accept
post :reject
end
end
and put something like this in your OffersController
def accept
offer = current_user.offers_received.find(params[:id])
offer.accept
end
# similarly for reject
Then you can issue a POST request to offers/3/accept
and it will cause the offer with id 3 to be accepted. Something like this in a view should do it:
link_to "Accept this offer", accept_offer_path(@offer), method: :post
Note that I didn't just write Offer.find(params[:id])
because then a crafty user could accept offers on the behalf of the seller. See Rails Best Practices.