Skip to content
This repository has been archived by the owner on Mar 29, 2018. It is now read-only.

Commit

Permalink
Add support for currency in Cart and Product
Browse files Browse the repository at this point in the history
  • Loading branch information
wezm committed May 4, 2009
1 parent 84709ac commit 8290752
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 23 deletions.
9 changes: 5 additions & 4 deletions app/models/cart.rb
Expand Up @@ -2,9 +2,10 @@ class Cart
attr_reader :items
attr_accessor :id
attr_accessor :gst_charged
attr_accessor
attr_accessor :currency

def initialize(options = {:gst_charged => true})
def initialize(currency, options = {:gst_charged => true})
@currency = currency
@items = []
options.each_pair {|k, v| self.send(:"#{k}=", v) }
end
Expand Down Expand Up @@ -62,15 +63,15 @@ def gst_charged?
def ex_gst_total
grand_total = 0
items.each do |item|
grand_total += item.subtotal(false)
grand_total += item.subtotal(@currency, false)
end
grand_total
end

def total
grand_total = 0
items.each do |item|
grand_total += item.subtotal(@gst_charged)
grand_total += item.subtotal(@currency, @gst_charged)
end
grand_total
end
Expand Down
10 changes: 5 additions & 5 deletions app/models/cart_item.rb
Expand Up @@ -12,10 +12,10 @@ def code
@product.code
end

def subtotal(gst_charged = false)
def subtotal(currency, gst_charged = false)
subtotal = 0
if price
subtotal = price * @quantity
if price(currency)
subtotal = price(currency) * @quantity
subtotal -= @coupon.discount_per_order if coupon?
if gst_charged
subtotal = round_to_cents(subtotal * 1.10)
Expand All @@ -34,8 +34,8 @@ def apply_coupon(coupon)

private

def price
@upgrade ? @product.upgrade_price : @product.price_for_quantity(@quantity)
def price(currency)
@upgrade ? @product.upgrade_price(currency) : @product.price_for_quantity(@quantity, currency)
end

def round_to_cents(amount)
Expand Down
64 changes: 52 additions & 12 deletions app/models/product.rb
@@ -1,3 +1,5 @@
class CurrencyException < Exception; end

class Product < ActiveRecord::Base
validates_exclusion_of :code, :in => %w{checkout cart eula}
# path segment, see http://www.w3.org/Addressing/URL/5_URI_BNF.html
Expand All @@ -8,7 +10,25 @@ class Product < ActiveRecord::Base

has_many :product_prices, :dependent => :destroy
has_many :coupons, :dependent => :destroy


BASE_CURRENCY = 'USD'

def self.exchange_rate_for_currency(currency)
return nil if currency == BASE_CURRENCY

rate_key = "currency.#{BASE_CURRENCY.downcase}-#{currency.downcase}"
rate = Radiant::Config[rate_key]

raise CurrencyException, "No rate for currency '#{currency}'" if rate.nil?
raise CurrencyException, "Rate for currency '#{currency}' is zero" if rate.to_f < 0.1
return rate.to_f
end

def base_currency
# Return a constant for now, in the future this might be an attribute
BASE_CURRENCY
end

def product_price_for_quantity(quantity)
self.product_prices.find(:first, :conditions => ['min_quantity <= ? AND `upgrade` != 1', quantity], :order => 'min_quantity desc')
end
Expand All @@ -17,34 +37,54 @@ def first_product_price
self.product_prices.find(:first, :order => 'min_quantity')
end

def upgrade_price
def upgrade_price(currency)
pp = self.product_prices.find(:first, :conditions => { :upgrade => true })
pp && pp.price.to_f
pp && price_in_currency(pp.price.to_f, currency)
end

def price_for_quantity(quantity)
def price_for_quantity(quantity, currency)
pp = product_price_for_quantity(quantity)
pp && pp.price.to_f
pp && price_in_currency(pp.price.to_f, currency)
end

def total_for_quantity(quantity)
price_for_quantity(quantity) * quantity
def total_for_quantity(quantity, currency)
price_for_quantity(quantity, currency) * quantity
end

def price_and_total_for_quantity(quantity)
[price_for_quantity(quantity), total_for_quantity(quantity)]
def price_and_total_for_quantity(quantity, currency)
[price_for_quantity(quantity, currency), total_for_quantity(quantity, currency)]
end

# savings_for_quantity(quantity)
# returns percent that the unit price is more then price for that quantity
# (ie if there is a 25% discount for the higher quantity, this will return
# 50)
# [ian: a. used price rather then total as quantity was common to both sides of division, b. should this show the discount percentage instead?]
def savings_for_quantity(quantity)
price_for_1 = price_for_quantity(1)
price = price_for_quantity(quantity)
def savings_for_quantity(quantity, currency)
price_for_1 = price_for_quantity(1, currency)
price = price_for_quantity(quantity, currency)
return 0.00 if price_for_1 <= 0.0001 # dont divide by near zero
100.0*(price_for_1 - price)/price_for_1
end

# Rounds to nearest 5 minus 1
def snap_to_round_amount(amount)
snap_to = 5
sign = amount < 0 ? -1 : 1
amount = amount.abs
quotient, modulus = amount.divmod(snap_to)

rounded_amount = quotient * snap_to + (modulus >= snap_to / 2.0 ? snap_to : 0)
rounded_amount = snap_to if rounded_amount == 0
return sign * rounded_amount - sign
end

private

def price_in_currency(amount, currency)
return amount if amount.nil? or currency.upcase == base_currency

snap_to_round_amount(amount * self.class.exchange_rate_for_currency(currency))
end

end
74 changes: 74 additions & 0 deletions spec/models/cart_spec.rb
@@ -0,0 +1,74 @@
require File.dirname(__FILE__) + '/../spec_helper'

describe Cart do

describe 'when product is not an upgrade' do

describe 'ex_gst_total' do

it 'delegates to the cart item, passing currency and gst_charged' do
p = Product.new
cart = Cart.new('XTS', :gst_charged => true)

cart_item = mock(CartItem)
CartItem.stub!(:new).and_return(cart_item)

cart_item.stub!(:quantity).and_return(1)
cart_item.should_receive(:subtotal).with('XTS', false).and_return(88.95)

cart.add_product_or_increase_quantity(p, 1)
cart.ex_gst_total.should == 88.95
end

end

describe 'total' do

it 'delegates to the cart item, passing currency and gst_charged' do
p = Product.new
cart = Cart.new('XTS', :gst_charged => true)

cart_item = mock(CartItem)
CartItem.stub!(:new).and_return(cart_item)

cart_item.stub!(:quantity).and_return(1)
cart_item.should_receive(:subtotal).with('XTS', true).and_return(89.95)

cart.add_product_or_increase_quantity(p, 1)
cart.total.should == 89.95
end

end

describe 'gst_amount' do

it 'is total minus ex_gst_total' do
p = Product.new
cart = Cart.new('XTS', :gst_charged => true)

cart.should_receive(:ex_gst_total).with(no_args()).and_return(20)
cart.should_receive(:total).with(no_args()).and_return(30)

cart.add_product_or_increase_quantity(p, 1)
cart.gst_amount.should == 10
end

end

end

describe 'when product is an upgrade' do

it "uses product.upgrade to calculate price when product is an upgrade" do
p = Product.new
cart = Cart.new('XTS', :gst_charged => false)

p.should_receive(:upgrade_price).with('XTS').twice.and_return(99.95)

cart.add_product_or_increase_quantity(p, 1, true)
cart.total.should == 99.95
end

end

end
104 changes: 102 additions & 2 deletions spec/models/product_spec.rb
@@ -1,13 +1,113 @@
require File.dirname(__FILE__) + '/../spec_helper'

describe Product do

describe 'price_for_quantity' do

it 'should be product_price.price when the supplied currency is the base currency' do
p = Product.new
product_price = stub(ProductPrice, :price => 99.95)
p.stub!(:product_price_for_quantity).and_return(product_price)
p.price_for_quantity(1, p.base_currency).should == 99.95
end

it 'should perform currency conversion and rounding when supplied currency is not the base currency' do
p = Product.new
product_price = stub(ProductPrice, :price => 50)
p.stub!(:product_price_for_quantity).and_return(product_price)
Radiant::Config.should_receive(:[]).with('currency.usd-aud').and_return('2')
p.price_for_quantity(1, 'AUD').should == 99
end

end

describe 'upgrade_price' do

it 'should be product_price.price when the supplied currency is the base currency' do
p = Product.new
product_price = stub(ProductPrice, :price => 24.95)
product_prices = mock('product_prices')
p.should_receive(:product_prices).and_return(product_prices)
product_prices.should_receive(:find).with(
:first,
:conditions => {:upgrade => true}
).and_return(product_price)

p.upgrade_price(p.base_currency).should == 24.95
end

it 'should perform currency conversion and rounding when supplied currency is not the base currency' do
p = Product.new
product_price = stub(ProductPrice, :price => 24.95)
product_prices = mock('product_prices')
p.should_receive(:product_prices).and_return(product_prices)
product_prices.should_receive(:find).with(
:first,
:conditions => {:upgrade => true}
).and_return(product_price)
Radiant::Config.should_receive(:[]).with('currency.usd-aud').and_return('2')

p.upgrade_price('AUD').should == 49.0
end

end

describe 'total_for_quantity' do
it 'should multiply price_for_quantity by qty' do
p = Product.new
p.stub!(:price_for_quantity).and_return(12.05)
p.total_for_quantity(3).should == 12.05*3
p.total_for_quantity(3, p.base_currency).should == 12.05*3
end
end

def self.should_snap(amount, expected_amount)
it "should snap #{amount} to #{expected_amount}" do
Product.new.snap_to_round_amount(amount).should == expected_amount
end
end

describe 'snap_to_round_amount' do

should_snap 1, 4.0
should_snap 2.25, 4.0
should_snap 3.9, 4.0
should_snap 100.12345, 99.0
should_snap 8.9823, 9.0
should_snap 480.39, 479.0
should_snap 7.39, 4.0
should_snap 83.398, 84.0
should_snap 47.19, 44.0
should_snap 7, 4
should_snap -1, -4
should_snap -3, -4
should_snap -7, -4
should_snap -7.6, -9
should_snap -11, -9

end

describe 'exchange_rate_for_currency' do

it 'should should be nil when currency is base_currency' do
Radiant::Config.should_not_receive(:[]).with(anything())
Product.exchange_rate_for_currency(Product::BASE_CURRENCY).should be_nil
end

end
it 'should be a float when currency is not base_currency' do
Radiant::Config.should_receive(:[]).with('currency.usd-aud').and_return('1.31')
Product.exchange_rate_for_currency('AUD').should be_close(1.31, 0.01)
end

it 'should raise a CurrencyException if the rate is not available' do
Radiant::Config.should_receive(:[]).with('currency.usd-aud').and_return(nil)
lambda { Product.exchange_rate_for_currency('AUD') }.should raise_error(CurrencyException)
end

it 'should raise a CurrencyException if the rate is very small' do
Radiant::Config.should_receive(:[]).with('currency.usd-aud').and_return('0.09')
lambda { Product.exchange_rate_for_currency('AUD') }.should raise_error(CurrencyException)
end

end

end

0 comments on commit 8290752

Please sign in to comment.