calculating royalties based on ranges in rails 3
I've built an application that tracks sales of books and is -- hopefully -- going to calculate author royalties.
Right now, I track sales in orders. Each order has_many :line_items. When a new line item is saved, I calculate the total sales of a given product, so I have a total sales count.
Each author has multiple royalty rules based on their contract. For example, 0 to 5000 copies sold, they get 10 percent. 5001 to 10,000, they get 20 percent. At开发者_C百科 first, I was calculating the author's share per line item. It was working well, but then I realized that my app is choosing which royalty rule to apply based on the total sales. If I post a big order, it's possible that the author's royalties would be calculated at the high royalty rate for the entire line item when in fact, the royalty should be calculated based on both the lower and high royalty rate (as in, one line item pushes the total sales passed the royalty rule break point).
So my question is how to best go about this. I've explored using ranges but this is all a little new to me and the code is getting a little complex. Here's the admittedly clunky code I'm using to pull all the royalty rules for a given contract into an array:
def royalty_rate
@product = Product.find_by_id(product_id)
@total_sold = @product.total_sold
@rules = Contract.find_by_product_id(@product).royalties
... where next?
end
@rules has :lower and :upper for each royalty rule, so for this product the first :lower would be 0 and the :upper would be 5000, then the second :lower would be 5001 and the second :upper would be 10,000, and so on.
Any help or ideas with this would be appreciated. It's actually the last step for me to get a fully working version up I can play with.
I was using this code below to pick out a specific rule based on the value of total_sold, but again, that has the effect of taking the cumulative sales and choosing the highest royalty rate instead of splitting them.
@rules = @contract.royalties.where("lower <= :total_sold AND upper >= :total_sold", {:total_sold => @total_sold}).limit(1)
Thanks in advance.
It sounds like you need to store the royalty calculation rules separately for each author -- or perhaps you have several schemes and each author is associated with one of them?
For the first case, perhaps something like this:
class Author
has_many :royalty_rules
end
class RoyaltyRule
belongs_to :author
# columns :lower, :upper, :rate
end
So when an Author is added you add rows to the RoyaltyRule model for each tier. Then you need a method to calculate the royalty
class Author
def royalty(product)
product = Product.find_by_id(product.id)
units = product.total_sold
amount = 0
royalty_rules.each do |rule|
case units
when 0
when Range.new(rule.lower,rule.upper)
# reached the last applicable rule -- add the part falling within the tier
amount += (units - rule.lower + 1) * rule.rate
break
else
# add the full amount for the tier
amount += (rule.upper - rule.lower + 1) * rule.rate
end
end
amount
end
end
And some specs to test:
describe Author do
before(:each) do
@author = Author.new
@tier1 = mock('tier1',:lower=>1,:upper=>5000,:rate=>0.10)
@tier2 = mock('tier2',:lower=>5001,:upper=>10000,:rate=>0.20)
@tier3 = mock('tier3',:lower=>10001,:upper=>15000,:rate=>0.30)
@author.stub(:royalty_rules) { [@tier1,@tier2,@tier3] }
end
it "should work for one tier" do
product = mock('product',:total_sold=>1000)
@author.royalty(product).should == 100
end
it "should work for two tiers" do
product = mock('product',:total_sold=>8000)
@author.royalty(product).should == (5000 * 0.10) + (3000 * 0.20)
end
it "should work for three tiers" do
product = mock('product',:total_sold=>14000)
@author.royalty(product).should == (5000 * 0.10) + (5000 * 0.20) + (4000 * 0.30)
end
# edge cases
it "should be zero when units is zero" do
product = mock('product',:total_sold=>0)
@author.royalty(product).should == 0
end
it "should be 500 when units is 5000" do
product = mock('product',:total_sold=>5000)
@author.royalty(product).should == 500
end
it "should be 500.2 when units is 5001" do
product = mock('product',:total_sold=>5001)
@author.royalty(product).should == 500.2
end
end
Notes: Author.royalty_rules
needs to return the tiers sorted low to high. Also, the lowest tier starts with 1 instead of 0 for easier calculation.
So, I don't get why you can't just calculate on total quantity sold?
I'll assume they don't need to know at the exact moment of order, so why not calculate, based on quantity sold as of yesterday.
E.g., Run a rake task, in the morning (let's say) that uses the following module called RoyaltyPayments
in a file in lib/royalty_payments.rb
Do something like
Module RoyaltyPayments
def royalty_range(total_sold, product_price)
sold_price = total_sold * product_price
leftover = total_sold % 5000
case total_sold
when 0..5000
total_sold * 0.1
when 5001..10000
((sold_price * 0.10)*5000) + ((sold_price * 0.2)*leftover)
else
((sold_price * 0.10)*5000) + (sold_price * 0.20)*5000) + ((sold_price * 0.3)*(total_sold - 10000)
end
end
Then make lib/tasks/royalty_payments.rake
In that file put something like:
include RoyaltyPayments
namespace :royalty_payments
desc "Make royalty calculations"
task :calculate_latest_totals
Product.all.each do |product|
total_sold = product.total_sold
royalty_range(total_sold, product.price)
end
Something like that.
You can do ranges of variables, eg. min..max
$irb
> min = 0
=> 0
> max = 5000
=> 5000
> min..max
=> 0..5000
> (min..max).class
=> Range
% is Numeric.modulo see http://www.ruby-doc.org/core/classes/Numeric.html#M000969 for details.
精彩评论