Methods depending on each other - ruby-on-rails

I'm building a complex billing system with Ruby on Rails. I have an Invoice model that has many items and payments. The invoice is considered paid when the paid_cents are greater than or equal to the total_cents.
Everything worked fine until I added support for late fees. Obviously, these fees should only be calculated when the invoice hasn't been paid. For instance, if the invoice was due on 2014-01-01, it has been paid on 2014-01-05 and today is 2014-01-24, the fees should be calculated for 4 days only.
In order to accomplish that, I added a check to the late_days method, making it return 0 if paid_cents was greater than or equal to total_cents AND the last payment was delivered before the invoice's due date.
Here's the issue: the total_cents are calculated summing the items' amounts and the late fees. In turn, the late_fee_cents method calls late_days which relies on total_cents to check whether the invoice has been paid.
This results in an infinite loop where the methods will call each other till the end of time (or someone pulls the plug).
Here's some code from my model:
def daily_late_fee_percentage
return 0 unless late_fee_percentage && late_fee_interval
late_fee_percentage.to_f / late_fee_interval.to_f
end
def daily_late_fee_cents
incomplete_total_cents.to_f / 100.0 * daily_late_fee_percentage
end
def late_days
return 0 unless due_at
days = Date.today - due_at
days = 0 if days < 0
days.to_i
end
def late_fee_cents
daily_late_fee_cents * late_days
end
def total_cents
incomplete_total_cents + late_fee_cents
end
I feel like I'm completely missing something obvious.
Any help would be much appreciated!

How should late_fee_cents be calculated? Is it only a fraction of the invoice's initial "total cents" excluding "late fees" multiplied by the days the invoice is overdue, or is it a fraction of the initial total cents plus late fees multiplied by the days the invoice is overdue?
If the former is true, then this might work:
We can define total_cents like this:
def total_cents(include_late_fees = true)
include_late_fees ? incomplete_total_cents + late_fee_cents : incomplete_total_cents
end
And late_days like this:
def late_days
return 0 unless due_at
return 0 if paid_cents >= total_cents(false) && (payments.last.try(:delivered_at) || due_at) <= due_at
days = Date.today - due_at
days = 0 if days < 0
days.to_i
end
Note that in late_days we are using total_cents(false).
This should eliminate the infitie loop.
If the latter is true, then first write down the exact mathematical formula for calculating the total invoice fee, including late fees and make sure
it doesn't have infinite recursion. The formula might contain sigma (Σ) or (Π) expressions.
Then it should be easy to convert it to code.
EDIT:
I think what you are trying to achieve is basically charging an interest on the debt (overdue fee). This is similar to computing compound interest.

Related

Getting number of events within a season based on date

I have a model called Event with a datetime column set by the user.
I'm trying to get a total number of events in each season (spring, summer, fall, winter).
I'm trying with something like:
Event.where('extract(month from event_date) >= ? AND extract(day from event_date) >= ? AND extract(month from event_date) < ? AND extract(day from event_date) < ?', 6, 21, 9, 21).count
The example above would return the number of events in the Summer, for example (at least in the northern hemisphere).
It doesn't seem like my example above is working, i'm getting no events returned even though there are events in that range. I think my order of operations (ands) may be messing with it. Any idea of the best method to get what I need?
Edit: actually looking at this more this will not work at all. Is there anyway select dates within a range without the year?
Edit 2: I'm trying to somehow use the answer here to help me out, but this is Ruby and not SQL.
require 'date'
class Date
def season
day_hash = month * 100 + mday
case day_hash
when 101..320 then :winter
when 321..620 then :spring
when 621..920 then :summer
when 921..1220 then :fall
when 1221..1231 then :winter
end
end
end
You can concat the month and day and query everything in between.
e.g 621..921
In SQL it would be something like
SUMMER_START = 621
SUMMER_END = 921
Event.where("concat(extract(month from event_date), extract(day from event_date)) > ? AND concat(extract(month from event_date), extract(day from event_date)") < ?,
SUMMER_START, SUMMER_END)
This can be easily made into a scope method that accepts a season (e.g 'winter'), takes the appropriate season start and end and returns the result.
This is what I ended up doing:
in lib created a file called season.rb
require 'date'
class Date
def season
day_hash = month * 100 + mday
case day_hash
when 101..320 then :winter
when 321..620 then :spring
when 621..920 then :summer
when 921..1220 then :fall
when 1221..1231 then :winter
end
end
end
in lib created a file called count_by.rb:
module Enumerable
def count_by(&block)
list = group_by(&block)
.map { |key, items| [key, items.count] }
.sort_by(&:last)
Hash[list]
end
end
Now I can get the season for any date, as well as use count_by on the model.
So then ultimately I can run:
Event.all.count_by { |r| r.event_date.season }[:spring]

Summing an array of numbers in ruby and extracting time over 40 hours

I wrote a payroll type app which takes a clock_event and has a punch_in and punch_out field. In a the clock_event class I have a method that takes the employee, sums their total hours and exports to CSV. This gives total hours for the employee, and then a total sum of their total_hours which is a method in the class calculated on the fly.
Here is my code:
clock_event.rb
def total_hours
self.clock_out.to_i - self.clock_in.to_i
end
def self.to_csv(records = [], options = {})
CSV.generate(options) do |csv|
csv << ["Employee", "Clock-In", "Clock-Out", "Station", "Comment", "Total Shift Hours"]
records.each do |ce|
csv << [ce.user.try(:full_name), ce.formatted_clock_in, ce.formatted_clock_out, ce.station.try(:station_name), ce.comment, TimeFormatter.format_time(ce.total_hours)]
end
records.map(&:user).uniq.each do |user|
csv << ["Total Hours for: #{user.full_name}"]
csv << [TimeFormatter.format_time(records.select{ |r| r.user == user}.sum(&:total_hours))]
end
csv << ["Total Payroll Hours"]
csv << [TimeFormatter.format_time(records.sum(&:total_hours))]
end
end
end
This method works and exports a CSV with all total time entries for each day then a sum of the hours at the bottom of the CSV file.
Here's my problem...
I can sum no problem, but I need to show the following:
Total Sum of hours (done)
Any hours over 40 hours I need to pull that amount into another field in the CSV file. So if an employee hits 40 hours it will show the 40 hours in one field then show the remaining ours as overtime right next to it.
I know how to sum the hours but am unsure as to how to extra the hours over 40 into another field into the CSV.
I'm sure there's a Ruby way to do it but I'm not certain on how this would work.
Any help would be greatly appreciated. If you need more code or context, please let me know.
Firstly, you should let total_hours return exactly what it says it returns in its method definition, the "total hours". Include your time formatting within this method to avoid scattering TimeFormatter logic through out your codebase:
def total_hours
TimeFormatter.format_time(self.clock_out.to_i - self.clock_in.to_i)
end
Second, you need to calculate overtime hours situationally, that will look like this:
def overtime_hours(num_weeks = 1, hours_per_week = 40)
ot = total_hours - (num_weeks * hours_per_week)
ot > 0 ? ot : 0
end
This gives you some customization so that if your business rules ever change you can actually maintain your code a little easier. Your defaults will always be 1 week and 40 hours, however you may do this:
ce.overtime_hours(2, 40)
or:
ce.overtime_hours(2, 39)
Some people only work 39 hours so that businesses can avoid paying for healthcare and other mandatory benefits that kick in at 40 hours. This will give you the ability to control what overtime_hours outputs.
And in your CSV for Week 1:
records.where('user_id is ? and date > ? and date < ?', user.id, begin_week_1, end_week_1).sum(&:overtime_hours)
and week 2:
records.where('user_id is ? and date > ? and date < ?', user.id, begin_week_2, end_week_2).sum(&:overtime_hours)

Rails map two fields in form to one field in model

I am using Rails 4.2rc3 and the simple_form gem.
I have an Event model which has a field reservations_open_at which indicates how many seconds before the event starts that reservations can start to be made (e.g. reservations might open up 2 days in advance, or maybe 10 hours in advance).
However, I want to use a form with a text input for a number (reservations_open_at_amount) and a select form for either 'Hours' or 'Days' (reservations_open_at_unit). I.e. the user can input '2 days' or '10 hours' via the two fields.
I can't quite figure out the right way to use virtual attributes to set the reservations_open_at field in seconds using a regular #event.update(event_params) call in my controller.
I don't need to remember whether the user chose Hours or Days when editing an existing record; if the reservations_open_at amount modulo (24*60*60) is zero (i.e. the number of seconds is an exact multiple of days) then I will display the form with reservations_open_at / (24*60*60) and Days, else reservations_open_at / (60*60) and Hours.
You are already more than half way there.
In the model, add two virtual attributes, time_amount, time_units. Add these 2 to the views.
Add a before_save callback to set the value in reservations_open_at.
Something like:
Edit: add getter for time unit
class Event < ActiveRecord::Base
attr_accessor :time_unit, time_amount
before_save :calculate_open_at
...
def time_unit
return #time_unit if #time_unit
if ( self.time_amount = reservations_open_at / 1.day.seconds ) > 0
#time_unit = 'days'
else
self.time_amount = reservations_open_at / 1.hour.seconds
#time_unit = 'hours'
end
end
def calculate_open_at
conversion_rate = time_unit == "days" ? 1.day.seconds : 1.hour.seconds
self.reservations_open_at = time_amount * conversion_rate
end
...
end

Rails: How To Deal With Many Subsets of Data From One Model?

Rails 3.0.3 application (stuck on a Dreamhost shared server).
I have a page that displays averages calculated from subsets of data from one model.
Right now, each average is calculated individually, like this:
From the view, I'm using the current_user helper provided by Devise authentication to call the average methods that are located in the user model, like so:
<%= current_user.seven_day_weight_average %>
<%= current_user.fourteen_day_weight_average %>
<%= current_user.thirty_day_weight_average %>
Here's the public methods and the averaging method in the user model:
def seven_day_weight_average
calculate_average_weight(7)
end
def fourteen_day_weight_average
calculate_average_weight(14)
end
def thirty_day_weight_average
calculate_average_weight(30)
end
. . .
private
def calculate_average_weight(number_days)
temp_weight = 0
weights_array = self.weights.find_all_by_entry_date(number_days.days.ago..Date.today)
unless weights_array.count.zero?
weights_array.each do |weight|
temp_weight += weight.converted_weight
end
return (temp_weight/weights_array.count).round(1).to_s
else
return '0.0'
end
end
This doesn't seem very efficient - the database is queried for every average calculated.
How can I calculate and make these averages available to the page with one database query?
You could cache an array of converted weights for the last 30 days (presuming 30 is the maximum days back), something like this:
def calculate_average_weight(number_days)
#converted_weights ||= weights.where("entry_date > ?", 30.days.ago).group_by(&:entry_date).sort_by do |date,weights|
date
end.collect do |date,weights|
weights.collect(&:converted_weight)
end
weights_during_period = #converted_weights[0..number_days-1].flatten
weights_during_period.sum / weights_during_period.length
end
Explanation:
Firstly, ||= gets or sets #converted_weights (ie don't bother setting it unless it's nil or false). This ensures only one db hit. Next, we find all weights from 30 days ago and group by date. This returns an array of [date, weights], which we sort by date. Then we collect the converted weights for each date, so we end up with: [weights on day 1], [weights on day 2], ....
Now, the calculation: we store values spanning the number of days from the array in weights_during_period. We flatten the values and calculate the average value.

How to get a variable to have 2 decimals

I have a variable i would like to force to have 2 and always 2 decimals. Im comparing to a currency. Often i get a comparison looking like the following.
if self.price != price
#do something
end
Where self.price = 120.00 and price = 120.0. The self.price is set with a :precision => 2 in the model, but how do i do the same with a variable, cause this seems to fail in comparison
Use integers for storing currency, for example, use store 100 cents for 1 dollar. It reduces headaches and may improve performance if it matters.
class Numeric
def round_to( decimals=0 )
factor = 10.0**decimals
(self*factor).round / factor
end
end
if self.price.round_to(2) != price.round_to(2)
#do something
end

Resources