Activity Descriptions

Create a migration that copies the product price into the line item, and change the add_product() method in the Cart model to capture the price whenever a new line item is created.

Author’s Solutions

Solution – search for “Add price to line item”

Readers’ Solutions

Marius says

The migration first: rails generate migration add_product_price_to_line_item price:decimal

class AddProductPriceToLineItem < ActiveRecord::Migration
  def self.up
    add_column :line_items, :price, :decimal, :precision => 8, :scale => 2

    say_with_time "Updating prices..." do
      LineItem.find(:all).each do |lineitem|
        lineitem.update_attribute :price, lineitem.product.price
      end
    end
  end

  def self.down
    remove_column :line_items, :price
  end
end
In app/models/cart.rb

def add_product(product_id, product_price)
  current_item = line_items.where(:product_id => product_id).first
  if current_item
    current_item.quantity += 1
  else
    current_item = LineItem.new(:product_id => product_id, :price => product_price)
    line_items << current_item
  end
  current_item
end
In app/controllers/line_items_controller.rb pass product.price as an argument in the Create action:

def create
  @cart = current_cart
  product = Product.find(params[:product_id])
  @line_item = @cart.add_product(product.id, product.price)
  session[:counter] = 0
  respond_to do |format|
    if @line_item.save
      format.html { redirect_to(@line_item.cart) }
      format.xml  { render :xml => @line_item, :status => :created, :location => @line_item }
    else
      format.html { render :action => "new" }
      format.xml  { render :xml => @line_item.errors, :status => :unprocessable_entity }
    end
  end
end

Brent says

The migration should update existing line items price with the product price. The add_product method will only take care of adding new products (after the migration).

Marius says

Thanks. I read over that.

Ken says

Would it also make sense to update app/models/line_item.rb to use the line_item’s price instead of the product’s price? This would make sure you are using the line_item price in the display:


class LineItem < ActiveRecord::Base
  belongs_to :product
  belongs_to :cart

  def total_price
    price * quantity
  end
end

Doc says

Through unit testing, I found that the price and quantity are not actually updated in the table for data added after the migration. I changed the add_product method to include a parameter for the current price (defaults to nil to use the catalog price if current_price is not provided) and used the update_attributes method to update the table with the new price and quantity. I chose to update the price any time the line item is changed.

File: app/models/cart.rb

...
  def add_product(product_id, current_price=nil)
    current_item = line_items.find_by_product_id product_id
    current_price = Product.find(product_id).price if current_price.nil?

    if current_item
      current_item.update_attributes \
        :quantity => current_item.quantity + 1,
        :price => current_price
    else
      current_item = LineItem.new \
        :product_id => product_id,
        :price => current_price
      line_items << current_item
    end

    current_item
  end
...

Elijah says

This will keep you from having to pass in the price:


def add_product(product_id)
    @product = Product.find(product_id)
    current_item = line_items.where(:product_id => @product.id).first
    if current_item
      current_item.quantity += 1
    else
      current_item=line_items.build(:product_id => @product.id,:price => @product.price)
      line_items << current_item
    end
    current_item
  end

Will says

In the examples given above, why is the “current_item” added to “line_items”? This code is not included in the book?

File: app/models/cart.rb

def add_product(product_id)
    ..
      line_items << current_item

Am I correct that the save method in “line_items_controller” already covers this?

File: app/controllers/line_items_controller.rb

def create
    ..
      if @line_item.save

Greg says

Am I missing something, or you overcomplicate the code of the Cart model?

I added one line to the add_product method:

File: app/models/cart.rb

      current_item.price = current_item.product.price

Karen says

This last proposed solution won’t work if current_item does not exist yet (the current item is not the same book as an item already in the cart.) You get a method not found error for price on the current item.

You do need to go back using the product_id to get the matching product and price if current_item does not exist…

else
 product = Product.find(product_id) 
      current_item = line_items.build(:product_id => product_id, :price => product.price)
end

for cases where line item refers to the same product as an item already in the cart, you stick to incrementing quantity alone….

Besides what I note above, I left everything else intact….

I like the Elijah solution the best thus far…..

Dean says

To add a product you must also consider the price, same product_id but a price change for a certain product makes a new line_item.


def add_product(product_id)
    @product = Product.find(product_id)
    current_item = line_items.where(:product_id => @product.id, :price => product.price).first
    if current_item
      current_item.update_attributes :quantity => current_item.quantity + 1
    else
      current_item = line_items.build(:product_id => product_id, :price => product.price)
      line_items << current_item
    end
    current_item
  end

Dave Buenconsejo says

The migration first: rails generate migration add_product_price_to_line_item price:decimal

class AddProductPriceToLineItem < ActiveRecord::Migration
  def self.up
    add_column :line_items, :price, :decimal, :precision => 8, :scale => 2
    LineItem.all.each do |line|
      line.update_attributes :price => line.product.price
    end
  end

  def self.down
    remove_column :line_items, :price
  end
end
In app/models/cart.rb

  def add_product(product_id)
    current_item = line_items.find_by_product_id(product_id)
    if current_item
      current_item.quantity += 1
    else
      current_item = line_items.build(:product_id => product_id)
      current_item.price = current_item.product.price
    end
    current_item
  end
In app/controllers/line_items_controller.rb pass product.price as an argument in the Create action:

def create
  @cart = current_cart
  product = Product.find(params[:product_id])
  @line_item = @cart.add_product(product.id)
  session[:counter] = 0
  respond_to do |format|
    if @line_item.save
      format.html { redirect_to(@line_item.cart) }
      format.xml  { render :xml => @line_item, :status => :created, :location => @line_item }
    else
      format.html { render :action => "new" }
      format.xml  { render :xml => @line_item.errors, :status => :unprocessable_entity }
    end
  end
end

Duccio Armenise says

The previous solutions didn’t work for me as the new column was created but the migration failed in copying the prices in.

I fixed it calling LineItem.reset_column_information right after adding the price attribute to the model:

rails generate migration add_price_to_line_item price:decimal

class AddPriceToLineItem < ActiveRecord::Migration
  def self.up

    add_column :line_items, :price, :decimal
    LineItem.reset_column_information

    LineItem.all.each do |li|
      li.update_attribute :price, li.product.price
    end

  end

  def self.down
    remove_column :line_items, :price
  end
end

I think this should be a best practice.

David says:

(Note that I used Rails 4.0.0)

The wording of the Activity Description hints at a simple implementation of what could be a more complex exercise. Capturing the price whenever a new line item is created means that the cart price is set to the product price at the time of the first Add to Cart operation. Even if you change the product price after this, subsequent additions of the same product to the same cart will use the price stored in the cart. This is probably the minimum behaviour that a customer would expect.

You can make this more complicated by an ambitious interpretation of the Activity Description. For example, you could create a new line item for every different price for each product item, but changing prices during purchase is unlikely to win friends in the customer community. Even more ambitious, you could maintain prices at the same value during a customer session, but we would need a more complex data model incorporating datetime-based product pricing.

The method I’ve used below assumes the same data model as in the book and that we set the cart line item price for any product from the product price at the time of the first Add to Cart operation for that line item.

  1. Create the migration
    1. Run the generation: rails generate migration add_product_price_to_line_item price:decimal
    2. Edit the resulting migration file: The migration generation command will create a file matching the pattern db/migrate/_add_product_price_to_line_items.rb. Edit that file, or the most recent file matching the pattern if there is more than one, to look like this:
      class AddProductPriceToLineItems < ActiveRecord::Migration
        def up
          add_column :line_items, :price, :decimal, :precision => 8, :scale => 2
      
          LineItem.find(:all).each do |lineitem|
            lineitem.update_attribute :price, lineitem.product.price
          end
        end
      
        def down
          remove_column :line_items, :price
        end
      end
      
  2. Change the line items controller to capture the product price: in the file app/controllers/line_items_controller.rb, change the line
    @line_item = @cart.add_product(product.id)
    to
    @line_item = @cart.add_product(product.id, product.price)
  3. Change the cart to pass the product price when adding a product to the line item controller: in the file app/models/cart.rb, edit the line
    def add_product(product_id)
    to add the product price as a parameter to add_product() like
    def add_product(product_id, product_price)
    and pass the product price to the new line item by changing
    current_item = line_items.build(product_id: product_id)
    to
    current_item = line_items.build(product_id: product_id, :price => product_price)
  4. Use the line item price to calculate line item total price*: in the file app/models/line_item.rb, change the line
    product.price * quantity
    to
    price * quantity

Test the changes by adding items to the cart and changing product prices between adding items.