开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜