calculating royalties based on ranges in rails 3 - ruby-on-rails

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

Related

Optimizing for maximum number of options to show customer given collection of their existing selections

This is a variation of an older question (linked below), that I phrased poorly so doesn't adequately address my problem.
BACKGROUND
Suppose there is an apple basket with apples, each of which is an object with a size attribute, and in this particular basket, we have 3 apples: [{size:'S'}, {size:'M'}, {size:'L'}]
In the shopping process, each time a customer adds an apple to the cart, they are given the option to select a size, but importantly, they do not have to select a size, it is an optional selector.
I'm trying to write a method, remaining_options that maximizes the maximum number of options a customer is shown when they add an apple to their cart, given their history of past selections. The fact that the size selection is optional is very important. Consider these 2 examples:
Example A: Customer selects an option
Customer adds 1st apple to cart
Customer sees prompt Please make a size selection (optional): [S, M, L]
Customer decides to select S
Customer adds 2nd apple to cart
Customer sees prompt Please make a size selection (optional): [M, L]
Example B: Customer does NOT select an option
Customer adds 1st apple to cart
Customer sees prompt Please make a size selection (optional): [S, M, L]
Customer skips this step
Customer adds 2nd apple to cart
Customer sees prompt Please make a size selection (optional): [S, M, L]
In Example B, because the customer did NOT select an option, the full set of options available is still shown. The code does not arbitrarily "remove" an apple from the option-set. In other words, this method remaining_options is not responsible for calculating remaining quantity, only remaining options, which it tries to maximize.
NOTE: Of course, even though this method doesn't calculate quantity, upon real world checkout, inventory is adjusted for the next customer, again via a different method. I.e., assuming that in the examples above, customer does not make a size selection for the 2nd apple, then upon checkout...
Example A, customer will receive 1 S apple and a 2nd apple that's either M or L, as randomly selected by other code. If customer receives [S,M], then the next customer that orders an apple will only be able to add 1 apple and remaining_options will only return [L]
Example B, customer will receive 2 random apples, could be [S,M], [M,L], [S,L], as randomly selected by other code. If customer receives [S,L], then the next customer that orders an apple will only be able to add 1 apple and remaining_options will only return [M]
COMPLEXITY
The challenge is I want remaining_options to work for any given number of possible attributes (size,color,price, etc.) on the apple object, and any number of options for such attributes. And since selections are optional, user can decline to select size,color,price,etc., or select 1 (e.g., only size) or 2 (e.g., size and price) and etc.
With this complexity, it's important that the code not treat the process of adding an apple to the cart as an independent process, where the remaining_options is calculated by options in basket minus the last option selected.
The code should instead look at all the apples in the customer's cart, whether or not an option has been selected for each, and then calculating the maximum number of remaining_options
The solutions in the previous posted question didn't work because they did the former, i.e., treated adding each apple as an independent process that removed an option from the basket.
Here's an example to clarify. Suppose the basket of 3 apples looks like this:
[
{size:'S', price:1},
{size:'M', price:2},
{size:'L', price:2},
]
Step 1 - adding the 1st apple
Let's say remaining_options takes an argument of the customer's existing. So when a customer adds their 1st apple to the cart, there's nothing in the cart existing, so all options are returned
basket.remaining_options([])
=> {size:['S','M','L'], price: [1,2]}
Step 2 - adding the 2nd apple
To finish up the 1st apple transaction, customer decided to select an apple with price:2. Then they add their 2nd apple to the cart. Since there's more than 1 apple with price:2, the customer's 1st choice created no capacity constraints, an again all options are shown.
basket.remaining_options([{price:2}])
=> {size:['S','M','L'], price: [1,2]}
NOTE: #obiruby astutely observed that although {size:['S','M','L'], price: [1,2]} is technically all the remaining options, they're not all selectable. For this second apple, if customer selects M, then the first apple has to be auto-assigned as L and therefore is no longer selectable. This is handled by yet another method that I already have working. So many methods... all to make this max-options optimization work!
Step 3 - adding the 3rd apple
To finish up the 2nd apple transaction, customer decided to, again, select an apple with price:2. Then they add their 3rd apple to the cart. Now it's tricky. If this code just subtracted options selected, then on face value, remaining_options might return this: {size:['S','M','L'], price: [1]}. But this is not accurate, because when all the price:2 apples are taken, so are all the M and L apples by default. This is why the right code needs to look collectively at existing cart. I.e., this should be the result:
basket.remaining_options([{price:2}, {price:2}])
=> {size:['S'], price: [1]}
Note also that because of how capacity constraints work, the following should also be returned correctly by the method:
basket.remaining_options([{price:2}, {size:'M'}])
=> {size:['S'], price: [1]}
# user's 1st ordered apple has a price request
# user's 2nd ordered apple has a size request
# COMBO of the 2 only work if...
# user's 1st ordered apple is the {price:2, size:'L'} apple in basket
# user's 2nd ordered apple is the {size:'M', price:'2'} apple in basket
# therefore if user is looking at remaining options for a 3rd apple, there's only the {size:'S', price:'1'} apple left in basket
So... yeah help greatly appreciated in writing this method! I have my non-working ideas in the old post. In looking at that proposed solution (which alas also doesn't look collectively at the existing cart), I realized that one solution could be to map out all the possible combinations of apples in basket that could be assigned to fulfill the requests of each apple in the cart, and picking the combination of assignments that yields the highest number of remaining options. But I feel like this solution would be wildly inefficient as baskets, carts, and possible options increased in size.
I think there are a couple of things that need to be considered before you're able to fully tackle this problem.
SKUs and stock
In an E-Commerce system, as was pointed out correctly in a comment, there's usually a concept of SKUs which uniquely describe a single product. So each of your unique apple types would be a separate SKU. In your simplified examples, the SKUs are for small, medium and large apples; in your "fully" example, you'd have separate SKUs for "small red apple", "medium red apple", "small green apple", "large green apple" etc. The fact that all of these are "apples" and the possible combinations of various attributes don't matter in terms of SKUs – these categorizations based on attributes is an arbitrary grouping criterion that is put on top of the list of all SKUs to make them more approachable to users of the system (be it customers, employees, tax advisors etc.).
Subsequently, each SKU has a quantity >= 0 in stock. Again, these quantities are completely independent of each other and the fact that you might say "we have 20 apples in stock" (where 5 of them are S, 11 M and 4 L) is a grouping approach that you put on top, not one that actually exists from a stock keeping perspective.
SKU/category assignment/locking
The 2nd issue to understand is the concept of article assignment/locking. That is, when someone adds an article to their cart, you temporarily mark the article as if it were already purchased so that it can't get sold twice. This approach is usually referred to as locking. Depending on order frequency, you then set a timeout of that lock, e.g. 30 minutes, meaning, if a purchase doesn't happen within 30 minutes, you release the lock and allow another customer to lock it (and ideally purchase it).
In a normal E-Commerce context, you would create such locks on a SKU level, but it sound like this is not the right approach in your case since you also allow users to add non-specific articles to their carts. So what you probably want is a kind of multi-level lock:
When a user adds a non-specific article to their cart, you lock the selected quantity of all categories that the article is assigned to.
When a user adds a specific SKU to their cart, you lock the selected quantity of the specific SKU for that user.
You add constraints that doesn't allow any category and any specific SKU to exceed their individual current stock.
Distribution
The last step then is actual distribution. Here, you go from most to least specific, i.e. you start by alotting the specific SKUs, followed by articles assigned to most categories all the way to the "I don't care, I just want any apple" customers.
I hope this makes sense to you. I'm aware that it's highly conceptual. But it's hard to give specific answers to a somewhat generic and (apparently) theoretical use case.
Your question seams like a NP-complete problem to me, I can't prove it though ^^
The following code is only a proof of concept for you to test.
Edit: What the code does is: Given the "abstract items" in the client's cart and the "real items" stock in the basket, generate all the combinations of "real items" (let's call these combinations "virtual carts") that satisfy both the client's cart and the stock in the basket. Then, for each "virtual cart", simulate the remaining stock in the basket. Finally, make the union of the remaining choices of each "virtual basket".
require 'set'
def extract_options(basket)
basket.each_with_object( {} ) do |item, options|
item.each do |attr,value|
options[attr] = Set.new unless options.has_key?(attr)
options[attr] << value
end
end
end
def expand_item(item, options)
expanded = [[]]
item.each do |attr,value|
opts = value.nil? ? options[attr] : [value]
expanded = expanded.each_with_object( [] ) do |path,exp|
opts.each { |val| exp << ( path.dup << [attr, val] ) }
end
end
expanded.map(&:to_h)
end
def remaining_options(basket, cart)
options = extract_options(basket)
return [options] if cart.empty?
remaining = []
f,*r = *cart.map { |item| expand_item(item, options) }
f.product(*r) do |cart|
b = cart.each_with_object( basket.dup ) do |item,bask|
break unless index = bask.index(item)
bask.delete_at(index)
end
remaining << extract_options(b) if b
end
remaining.uniq
end
def remaining_options_union(basket,cart)
remaining_options(basket,cart).each_with_object( {} ) do |remain,union|
remain.each do |attr,set|
union[attr] = Set.new unless union[attr]
union[attr] += set
end
end
end
Your test-case is small so the calculation is quick:
basket = [
{size:'S', price:1},
{size:'M', price:2},
{size:'L', price:2},
]
cart = []
remaining_options_union(basket,cart)
# => {:size=>#<Set: {"S", "M", "L"}>, :price=>#<Set: {1, 2}>}
cart << {size: nil, price: 2} # all attributes need to be populated
remaining_options(basket,cart)
# => [{:size=>#<Set: {"S", "L"}>, :price=>#<Set: {1, 2}>}, {:size=>#<Set: {"S", "M"}>, :price=>#<Set: {1, 2}>}]
remaining_options_union(basket,cart)
# => => {:size=>#<Set: {"S", "L", "M"}>, :price=>#<Set: {1, 2}>}
cart << {size: nil, price: 2}
remaining_options(basket,cart)
# => [{:size=>#<Set: {"S"}>, :price=>#<Set: {1}>}]
remaining_options_union(basket,cart)
# => {:size=>#<Set: {"S"}>, :price=>#<Set: {1}>}
Update: The following code keeps the previously calculated "virtual baskets" in memory for speeding up the addition of a new item to the cart.
require 'set'
class Cart
def self.extract_choices basket
basket.each_with_object( {} ) do |item, attrs|
item.each do |key,value|
raise "Null valued item attribute in basket" if value.nil?
attrs[key] = Set.new unless attrs.has_key?(key)
attrs[key] << value
end
end
end
attr_reader :remaining_options
def initialize basket
choices = self.class.extract_choices basket
#globs = choices # needed to unglob a null valued attribute in an item
#attrs = choices.keys # attributes found in the items of the basket
# The items in the basket shall have all the possible attributes populated and not null
basket.each do |item|
#attrs.each do |attr|
raise "Invalid item in basket" unless item[attr]
end
end
#virtual_baskets = [ basket ]
#remaining_options = choices
end
def <<(item)
# As we're using the 'Hash#==' method, each item must have all its
# attributes set (the same ones as in the basket), no more, no less.
item = #attrs.each_with_object( {} ) do |attr,obj|
obj[attr] = item[attr]
end
# NOTE: an item with globs will multiply the number of virtual baskets
update_virtual_baskets item
choices = #virtual_baskets.map{|b| self.class.extract_choices b}
#remaining_options = choices.each_with_object( {} ) do |choice, union|
choice.each do |attr,set|
union[attr] = Set.new unless union[attr]
union[attr] += set
end
end
end
private
def unglob item
expanded_items = [[]]
item.each do |attr, value|
choices = value.nil? ? #globs[attr] : [value]
expanded_items = expanded_items.each_with_object( [] ) do |pre, obj|
choices.each { |val| obj << ( pre.dup << [attr, val] ) }
end
end
expanded_items.map(&:to_h)
end
def update_virtual_baskets formated_item
expanded_items = unglob formated_item
valid_baskets = []
#virtual_baskets.each do |basket|
expanded_items.each do |item|
basket.each_index do |idx|
next unless basket[idx] == item
bsk = basket.dup
bsk.delete_at(idx)
valid_baskets << bsk
end
end
end
raise "Error while adding item to cart - no stock" if valid_baskets.empty?
#virtual_baskets = valid_baskets.uniq
end
end
You can use it like this:
basket = [
{origin:'IL', size:'S', color:'G', price:1},
{origin:'SP', size:'M', color:'G', price:2},
{origin:'SP', size:'M', color:'R', price:2},
{origin:'SP', size:'M', color:'Y', price:3},
{origin:'CA', size:'L', color:'G', price:1},
{origin:'SP', size:'L', color:'G', price:4},
{origin:'CA', size:'L', color:'R', price:4},
]
cart = Cart.new basket
cart << {price:2}
cart << {size:'M'}
cart << {origin:'SP'}
cart.remaining_options
# => {:origin=>#<Set: {"IL", "CA", "SP"}>, :size=>#<Set: {"S", "L", "M"}>, :color=>#<Set: {"G", "R", "Y"}>, :price=>#<Set: {1, 4, 3, 2}>}
cart << {price:2}
cart.remaining_options
# => {:origin=>#<Set: {"IL", "CA"}>, :size=>#<Set: {"S", "L"}>, :color=>#<Set: {"G", "R"}>, :price=>#<Set: {1, 4}>}
Possible improvements: By adding a "quantity" to the items in the basket you should be able to reduce the size and the number of "virtual baskets". Also, if the price is a mandatory field (which makes sense) then you can "kind of" split the problem by price and make the code faster.
Remark: Another way to phrase what you're looking for would be: For each possible choice in each attribute (ex. :size is an attribute and one of its possible choices is 'S'), does it exist a combination of items in the cart that doesn't deplete it ? Unless there exists a mathematical formula to find it quickly, you'll have to almost walk through all the possible combinations in order to be able to respond 'no' to that question...
To a certain extent, my second proposal that keeps in memory the valid "virtual baskets" of the previous iteration is a sensible way to tackle the problem.
I took a stab, please see below.
You can play around with the code by copying the entire code below into a single file (say, test.rb) and then running it from the command line (ie, ruby test.rb)
The design is based off the idea that the problem of "maximizing the options that remain in-basket" is logically equivalent to "removing options from the basket in order of most common to least common".
# test.rb
require 'minitest/autorun'
class Basket
attr_accessor :basket
def initialize(opts={})
#basket = opts[:basket]
end
def remaining_options(selected_options=[])
return {} if #basket.empty?
selected_options.each do |option_criteria|
select_option(option_criteria)
end
print_basket
end
def select_option(option_criteria)
selectable_options = #basket.select { |opt| matches?(opt, option_criteria) }
option_to_select = most_common_option(selectable_options)
remove_from_basket option_to_select
end
def most_common_option( selectable_options )
max_matches = 0
max_matches_idx = nil
selectable_options.each_with_index do |option, i|
option_matches = 0
option.keys.each do |k|
selectable_options.each do |opt, i|
option_matches += 1 if option[k] == opt[k]
end
if option_matches > max_matches
max_matches = option_matches
max_matches_idx = i
end
end
end
selectable_options[max_matches_idx]
end
def remove_from_basket( option_to_select )
idx_to_remove = #basket.index { |i| matches?(i, option_to_select)}
#basket.delete_at idx_to_remove
end
def matches?(opt, selected)
selected.keys.all? { |k| selected[k] == opt[k] }
end
def print_basket
hsh = Hash.new { |h,k| h[k] = [] }
#basket.each do |item_hsh|
item_hsh.each do |k, v|
hsh[k] << v unless hsh[k].include?(v)
end
end
hsh
end
end
class BasketTest < Minitest::Test
def setup_basket
Basket.new basket: [
{size:'S', price:1},
{size:'M', price:2},
{size:'L', price:2},
]
end
def test_initial_basket_case
basket = setup_basket
assert basket.remaining_options == {size:['S','M','L'], price: [1,2]}
end
def test_returns_empty_if_basket_empty
basket = Basket.new basket: []
assert basket.remaining_options([{price:2}]) == {}
end
def test_one_pick
basket = setup_basket
assert basket.remaining_options([{price:2}]) == {size:['S', 'L'], price: [1,2]}
end
def test_two_picks
basket = setup_basket
assert basket.remaining_options([{price:2}, {price:2}]) == {size:['S'], price: [1]}
end
def test_maximizes_options
larger_basket = Basket.new basket: [
{size:'S', price:1},
{size:'L', price:2},
{size:'M', price:2},
{size:'M', price:2}
]
assert larger_basket.remaining_options([{price:2}]) == {size:['S', 'L', 'M'], price: [1,2]}
end
def test_maximizes_options_complex
larger_basket = Basket.new basket: [
{size:'S', price:1, color: 'red'},
{size:'L', price:2, color: 'red'},
{size:'M', price:2, color: 'purple'},
{size:'M', price:2, color: 'green'},
{size:'M', price:2, color: 'green'},
{size:'M', price:2, color: 'green'}
]
assert larger_basket.remaining_options([{price:2}, {color: 'green'}]) == {size:['S', 'L', 'M'], price: [1,2], color: ['red', 'purple', 'green']}
end
end

Summing time by looking at an associated column- an easier way?

I have a Project model that has_many: Tasks each Task has_many :users, through: :userAssociations so one person can work on a task but in the case of a meeting or any collaboration many users can work on the same task.
Tasks have a column: t.integer "time" which allows users to state in minutes the time the task took.
In the Show method of the Project, I'd like to be able to sum the total time that the project has taken.
If not for the fact that a Task can have many users through the userAssociations model, I think this would be very easy: #project.tasks.sum(:time) but that won't double count the time if multiple users were working on the task.
Since originally posting this question I was able to get it working with this helper:
def project_time_spent
#timeSum = 0;
#tasks.each do |tsk|
#userTask = UserTask.where(task_id: tsk.id);
if #userTask.count(:id) > 1
#timeSum += tsk.time * #userTask.count(:id)
else
#timeSum += tsk.time
end
end
return #timeSum
end
But this doesn't feel very "Railsy". Do any Rails gurus recommend a cleaner approach? Am I missing out on an easy method?
You can do it a little easier than that since a task has many users. I think you can also use inject here.
def project_time_spent
#tasks.inject { |sum, tsk| sum + (tsk.time * tsk.users.count) }
end
You can include an instance method in your Project model:
def time_spent
self.tasks.inject { |sum, tsk| sum + (tsk.time * tsk.users.count) }
end
I wouldn't bother checking for count > 1. The savings getting rid of the multiply are minuscule.

Where to keep site-wide counters, Rails

Newb developer. Need to put some counters on the site that counts the number of times certain songs are played. It's related to the Song model, but doesn't have to be, and not of any one song instance. Just wondering where I put a counter that acts like a constant?
if Subscription.where("band_id = ? and fan_id = ?", #band.id, #fan.id).any? && #fan.donating
#song.total_sub_donate_plays += 1
#song.total_month_sub_donate_plays += 1
site_wide.counter_total +=1
site_wide.counter_total_month += 1
end
If you have memcached/redis setup then use the Rails.cache
http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-increment
something like:
if Subscription.where("band_id = ? and fan_id = ?", #band.id, #fan.id).any? && #fan.donating
#song.total_sub_donate_plays += 1
#song.total_month_sub_donate_plays += 1
Rails.cache.increment("stats/counters/total")
Rails.cache.increment("stats/counters/#{Date.current.strftime("%Y/%m")}", {expires_in: 40.days})
end
then later you can:
Rails.cache.read("stats/counters/total")
Rails.cache.read("stats/counters/#{Date.current.strftime("%Y/%m")}")
Another option is to create aggregate data tables. this will let you have more control in the long run.
something like:
class DailySongData
# event_date: date,
# sub_count:integer
# listing_count:integer
end
this will help organize your data for future learning

Rails update_column from within seeds.rb

I have a seed file that generates numerous objects, including accounts and account transactions. After the accounts are created, a loop generates 80 transactions for each account (there are 150 accounts).
Everything works well, except I need the account's balance column to be updated to reflect the change caused by the transaction. I've tried this in 100 different ways. Here is my most recent approach:
from seeds.rb
accounts.each do |account|
80.times do
type = types.sample
case (type)
when 1
description = descriptions_ATM_Withdrawal.sample
amount = (atm_amounts.sample) *-1
when 2
description = descriptions_Check.sample
amount = ((500.0 - 5.0) * rand() + 5) *-1
when 3
description = descriptions_Deposit.sample
amount = (2000.0 - 5.0) * rand() + 5
when 4
description = descriptions_AutoDraft.sample
amount = ((350.0 - 5.0) * rand() + 5) *-1
when 5
description = descriptions_POS.sample
amount = ((150.0 - 5.0) * rand() + 5) *-1
when 6
description = descriptions_Transfer
amount = (500.0 - 5.0) * rand() + 5
when 7
description = descriptions_Withdrawal
amount = ((500.0 - 5.0) * rand() + 5) *-1
when 99
description = descriptions_Miscellaneous
amount = ((500.0 - 5.0) * rand() + 5) *-1
end
AcctTransaction.create do |transaction|
transaction.id = SecureRandom.random_number(99999999999999)
transaction.account_id = account.id
transaction.transaction_type_id = type
transaction.description = description
transaction.amount = amount
transaction.adjusted_bal = account.balance + transaction.amount
# keep transaction in chronological order unless it's the first one
unless AcctTransaction.exists?(account_id: transaction.account_id)
transaction.date = rand(account.date_opened..Time.now)
else
transaction.date = rand(AcctTransaction.where(account_id: transaction.account_id).last.date..Time.now)
end
end
Account.find(AcctTransaction.last.account_id).update_column(:balance, AcctTransaction.last.adjusted_bal)
end
end
Trying to update with the last line before the "ends". I've tried update, update_attribute, as well as just "=". Nothing seems to work. The account's "balance" field MUST be updated after each transaction in order to provide an accurate basis for calculation in the next iteration of the account transaction creation loop.
Any suggestions will be considered. This can't be that difficult. Again, the whole thing would work just fine if the balance gets updated like it should.
Rails 4.1.8 / Ruby 2.1.5
Please help.. THANK YOU!
EDIT
account.rb:
class Account < ActiveRecord::Base
belongs_to :customer
belongs_to :user
has_one :acct_type
has_many :acct_transactions, :dependent => :destroy
accepts_nested_attributes_for :acct_type
accepts_nested_attributes_for :acct_transactions
validates :acct_type_id, presence: true
end
acct_transaction.rb
class AcctTransaction < ActiveRecord::Base
belongs_to :account
has_one :transaction_type
accepts_nested_attributes_for :transaction_type, :allow_destroy => false
end
screen shot of (wrong) results
As you can see, what happens is the original balance (seen in header if you zoom in), persists. Every transaction, therefore, is calculated based on this amount.
methods in acct_transactions_controller that actually work to perform these updates when the app is live. Trying to replicate this functionality when creating the nested seeds:
public
def modify_acct_balance
account = Account.find(#acct_transaction.account_id)
case #acct_transaction.transaction_type_id
when 1,2,4,5,7
account.update(balance: account.balance - #acct_transaction.amount)
when 3
account.update(balance: account.balance + #acct_transaction.amount)
end
end
def adjust_balance
case #acct_transaction.transaction_type_id
when 2,4,5,7
#acct_transaction.adjusted_bal = Account.find(#acct_transaction.account_id).balance - #acct_transaction.amount
when 3
#acct_transaction.adjusted_bal = Account.find(#acct_transaction.account_id).balance + #acct_transaction.amount
end
end
Please note that the above methods calculate differently based on the transaction type (+ or -) - user always supplies positive value. Works though.
How to reproduce this kind of functionality in the above seed file?
Thanks
There are many ways to do this, for sure, but at first glance it looks like your approach would work. My first thought is that the you have some life-cycle hooks or counter-cache configuration on the models that's conflicting with your logic. (You may want to show us your model definitions)
If that's not the case, I'd simply skip updating the balance after every transaction and calculate the balance at the end of the transaction-creation loop.
In pseudo:
accounts.each do |account|
80.times do
# create transactions
end
account.reload
account.balance = account.transactions.sum(:adjusted_bal)
account.save
end
Or even faster, build and cache the balance locally during transaction-creation:
accounts.each do |account|
balance = 0
80.times do
# create transaction
balance += transaction.amount
end
account.balance = balance
account.save
end
To make this work, I had to do 2 things:
Change the syntax of update method (last line of seeds.rb) to this : account.update(balance: transaction.adjusted_bal)
Add a ! (bang) to the create method (AcctTransaction.create! do |transaction|)
The create method probably works fine without the bang, but I'm leaving it in because everything works great now. Also, I'm not sure why using update only works with this syntax. I tried other variants and other methods to update this column and none seemed to work. Almost seems like seeds.rb only works with "bare bones" Rails methods. Thanks to the Deeno and all others who've helped me work this out.

Why this application controller won't return correct value to controller who called?

Why do I get empty when I execute this? Asumming User's point is 2500. It should return 83
posts_controller
#user = User.find(params[:id])
#user.percentage = count_percentage(#user)
#user.save
application_controller
def count_percentage(user)
if user
if user.point > 2999
percentage = ((user.point / 5000.0) * 100 ).to_i
elsif user.point > 1999
percentage = ((user.point / 3000.0) * 100 ).to_i
end
return percentage
end
end
It looks like there is some lack of basic understanding, so I try to focus on a few points here.
count_percentage belongs to your model. It is a method which does things that are tied to your User records. In your example, the code can only be used with a User record. Therefore it belongs to your User class!
class User < ActiveRecord::Base
def percentage
if self.point > 2999
return ((self.point / 5000.0) * 100 ).to_i
elsif user.point > 1999
return ((self.point / 3000.0) * 100 ).to_i
else
# personally, I'd add a case for points range 0...3000
end
end
As you said, you want to put that value into your "side menu", so this allows to e.g #user.percentage there which gives you the desired percentage.
According to your description (if I understood it the right way) you want to store the calculated value in your user model. And here we go again: Model logic belongs into your model class. If you want to keep your percentage value up to date, you'd add a callback, like:
class User < ActiveRecord::Base
before_save :store_percentage
def precentage
# your code here
end
private
def store_percentage
self.my_percentage_field_from_my_database = self.percentage
end
end
To your actual problem: user.point may be NULL (db) / nil (ruby), so you should convert it to_i before actually calculating with it. Also that's why I've suggested an else block in your condition; To return a value wether or not your user.point is bigger than 1999 (if it even is an integer.. which I doubt in this case).
To answer on this question you should try to examine user.point, percentage value in the count_percentage method.
I strongly recommend pry. Because it's awesome.

Resources