Activity Description

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

Author’s Solutions

blah

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
...
File: test/unit/cart_test.rb

require 'test_helper'

class CartTest < ActiveSupport::TestCase
  test "cart line item should save price" do
    cart = carts(:cart1)
    [:ruby, :loremipsum, :ruby].each do |which_product|
      product = products(which_product)
      item = cart.add_product product.id
      assert_equal item.price, product.price,
        "cart line item did not save price" 
    end
  end

  test "cart line item quantity should increment" do
    cart = carts(:cart2)
    product = products(:ruby)
    multiple = 3
    multiple.times { cart.add_product product.id }
    item = cart.line_items.find_by_product_id product.id    
    assert_equal multiple, item.quantity,
      "cart line item quantity is incorrect" 
  end

  test "cart line item price should equal most-recent price" do
    cart = carts(:cart3)
    product = products(:ruby)
    sale_price = 0.8 * product.price
    [product.price, sale_price, 100].each do |price|
      cart.add_product product.id, price
      item = cart.line_items.find_by_product_id product.id
      assert_equal price, item.price,
        "price #{item.price} is incorrect; should be #{price}" 
    end
  end

  test "cart should be destroyed" do
    cart = Cart.create
    assert cart.save, "unable to create cart" 
    cart.destroy
    begin
      cart = Cart.find(cart.id)
    rescue ActiveRecord::RecordNotFound
      cart = nil
    end
    assert_nil cart, "cart has not been destroyed" 
  end
end
File: test/fixtures/carts.yml

...
cart1: {}

cart2: {}

cart3: {}