Take the 2-minute tour ×

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 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?      

@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.

share|improve this question
add comment

3 Answers

up vote 2 down vote accepted

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

class RoyaltyRule
  belongs_to :author
  # columns :lower, :upper, :rate

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
        # add the full amount for the tier
        amount += (rule.upper - rule.lower + 1) * rule.rate

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] }

  it "should work for one tier" do
    product = mock('product',:total_sold=>1000)
    @author.royalty(product).should == 100

  it "should work for two tiers" do
    product = mock('product',:total_sold=>8000)
    @author.royalty(product).should == (5000 * 0.10) + (3000 * 0.20)

  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)

  # edge cases
  it "should be zero when units is zero" do
    product = mock('product',:total_sold=>0)
    @author.royalty(product).should == 0

  it "should be 500 when units is 5000" do
    product = mock('product',:total_sold=>5000)
    @author.royalty(product).should == 500

  it "should be 500.2 when units is 5001" do
    product = mock('product',:total_sold=>5001)
    @author.royalty(product).should == 500.2

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.

share|improve this answer
My royalties are actually stored under a Contracts Model. So, what I'm already doing is very similar. Author has_many :contracts and Contract has_many royalty_rules, so ... I get to royalties for the author via Author has_many :royalty_rules, :through => :contracts. It's not necessary to set Author.royalty_rules so it only pulls out the three columns I need, is it? –  Slick23 Jan 18 '11 at 16:59
So, the code above is working in the console. But when I put it in my view, I get "undefined method `total_sold' for nil:NilClass" -- which leads me to believe it's a problem with one of the products that's being returned. The total_sold columns all have values for all of the products in the development database. –  Slick23 Jan 18 '11 at 17:16
UPDATE: I figure it out. There was a slight problem with how the product ID was fed into the method, but it's fixed now and works fantastic. Thanks for your help! –  Slick23 Jan 18 '11 at 17:57
Glad it worked out. I should have realized you had a separate model for storing the rules. –  zetetic Jan 18 '11 at 18:24
add comment

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)
      ((sold_price * 0.10)*5000) + (sold_price * 0.20)*5000) + ((sold_price * 0.3)*(total_sold - 10000)

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)

Something like that.

share|improve this answer
Thank you ... I get how that's working and that would work. But I guess my real question is how do I pull in the ranges from the model instead of hard coding them? For example, instead of hardcoding 0..5000, I need to pull those numbers and populate the outer limits from the :lower and :upper columns in the Royalty model. That's what I'm having trouble with. The ranges change with each product and author. –  Slick23 Jan 18 '11 at 3:32
Oh, and one thing I'm not sure I get is what exactly leftover = total_sold % 5000 does? –  Slick23 Jan 18 '11 at 4:37
% divides and returns the remainder, basically. so if you have 10499 % 5000 the return value woudl be 499. How are you storing these values now? in a contract model I'd assume with the ranges stored? WE can extrapolate those figures, per author, and add them to this method. –  pjammer Jan 18 '11 at 14:22
Royalties are in their own model right now. It's set up as Contract has_many :royalty_rules and RoyaltyRules :belongs_to contract –  Slick23 Jan 18 '11 at 14:39
both the selected and answer and mine though don't store this information anywhere. You may want to think of a payments model, that stores the apyment you should be making, and then have PaymentTransaction's when a payment is actually made. –  pjammer Jan 18 '11 at 14:44
add comment

You can do ranges of variables, eg. min..max

> 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.

share|improve this answer
add comment

Your Answer


