How to decrease db attribute value over time with Rails? - ruby-on-rails

I'm creating a credit-based application, where the user puts credit in his account and the credit decreases over time based on user's active services.
For example:
User deposits U$ 30 in his account
User accquires a service that costs U$ 60/month
His credits will decrease over time, so in the second day of use, user's credit will be U$ 28 (U$ 30 - U$ 2 from the accquired service)
User will be able to see his current balance any time
I realize that I can use cron jobs to decrease the account balance, but it's gonna make a lot of requests to database and I am sure there must be better ways to do that.
How can I do it?

One way to achieve this is using a column and a callback e.g.
You will need to add a column say last_deducted_on as date then in the show action of account_balance you can have a before_action like
before_action :deduct_amount_if_applicable, only: :show
private
def deduct_amount_if_applicable
if current_user.deduction_applicable?
amt = (Date.today - account.last_deducted_on).to_i * amount_to_be_deducted
account.update_attribute(:total, (account.total - amt))
# update column with today's date
end
end
Although it will work, I suggest to use scheduler as it is a scheduled job and needs to be done every day, there are ways to reduce queries e.g. using update_all, etc. depending on use case but still the better option. In case if you have transaction history and you show date of transaction, above method will piss of user if he will open his account days later and suddenly huge amount is deducted.

Related

How to make a Job time safe? Ensuring it is run when it's supposed to using Date methods

We have CreditCard related rake tasks that are supposed to be run on the 1st every month, to remind our clients to update their payment method if it expired at some point during the previous month.
class SomeJob < ApplicationJob
def perform
::CreditCard.
.where(expiration_date: Date.yesterday.all_month)
.find_each do |credit_card|
Email::CreditCard::SendExpiredReminderJob.perform_later(credit_card.id)
end
end
end
I'm concerned that this particular Job as we currently have it might not be Time zone safe due to the Date.yesterday.all_month we use to get last month's date range (remember the rake task is run on the 1st every month).
For example, if for some reason the Job were to be run past midnight (on the 2nd), it would incorrectly notify clients with cards expiring this month when it should of notified last month's expired cards.
The safest bet would be to substract more than 1 day from Date, but I'm not sure that's the cleanest way to go (and later on someone would not understand why we are substracting 5, 7 days or whatever).
Are there safer ways to do it?
Date.current.prev_month.all_month
.current may be better than .today because it checks if it is set time zone, see answer.
.prev_month or .months_ago(1) see https://apidock.com/rails/v3.2.13/Date/prev_month

Show active record object to user based on time elapsed

I have this application which general goal is to poll users, aggregate their responses and create pie/bar charts.
My objective is while creating the questions set a time that will define when they are available for the users to see them and therefore answer to them.
What's the best approach to implement something like this? Maybe a background job that will automatically present the questions to the users or, probably when the users refresh the page where the questions should appear verify the elapsed time and present only the questions that are available in that day?
Use case: create a question that will be available tomorrow at 12h and will end at 13h.
Thanks
You can just use SQL.
scope :available, -> { where('start_date >= :time AND end_date <= :time', time: Time.current) }
and then use it in controller or where you want to.
You have good explanation here date scopes

Applying monthly referral discounts to a subscription fee

Description of functionality:
So I have a subscription based website that has two plans: Monthly and yearly. A monthly subscription is charged a monthly fee every month, while a yearly subscription is charged a one-time cumulative fee of 10 months, each year. ($29/mo and $290/yr)
I have 10% recurring referral discounts that are applied each month, for every active member who was referred (100% max). So if Bob signs up for a monthly subscription in October, and throughout the next month refers 5 people who sign up, he should have a 50% discount accumulated. This will be applied to his subscription fee in November, which will result in $14.50 for that month. But assume one of his referrals cancels in November. This will drop his discount for December by 10%, which will give him a 40% discount, with 4 active referrals, resulting in $17.40 for December.
Example of working code:
If every one of Bob's referrals is monthly, this is fairly simple to apply with webhooks. An amount is just deducted from Bob's current balance every time a successful payment goes through, if the user connected to that payment is one of Bob's referrals.
# Webhooks Controller
def payment_success
referred_user = User.find_by_customer_id(#subscription.customer.reference)
if referring_user = User.find_by_affiliate_code(referred_user.referral_code)
balance = referring_user.subscription.balance_in_cents
referring_user.subscription.adjustment({"amount_in_cents"=>"-290","memo"=>"Balance adjusted for referring #{referred_user.email}. Payment for referral was successful."})
referring_user.subscription.save
end
head :ok
end
This sends a request to Chargify, the payment processor I'm using, to adjust the balance accordingly.
My real problem:
It isn't this straightforward, though, when one of Bob's referrals is a yearly user.
Since the yearly subscription is a one-time payment, the payment_success webhooks aren't coming through every month, which means 10% of Bob's subscription can't be deducted each month from this yearly user's active referral.
I thought of just setting Bob's plan price to 10% less than standard, instead of adjusting the balance each month. This would require 9 different plans, though, since the plan price directly refers to a Plan object on Chargify, which points to its price attribute. That means that there would need to be a 90%_monthly_plan, a 80%_monthly_plan, and so on, which all live on the Chargify servers. This seems overly complex, and I also wouldn't be surprised if it caused other issues I'm not even considering at this point.
My current (unsatisfactory) solution:
So instead I decided to just create a scheduler on my server that is created when a yearly user signs up, and it fires every month.
I'm using rufus-scheduler for this. One important thing to note - Schedules are destroyed when the server is restarted, so I'm creating schedule records, and an initializer goes through these records and creates these schedules every time the server restarts.
# modified Webhooks Controller
def payment_success
referred_user = User.find_by_customer_id(#subscription.customer.reference)
if referring_user = User.find_by_affiliate_code(referred_user.referral_code)
balance = referring_user.subscription.balance_in_cents
referring_user.subscription.adjustment({"amount_in_cents"=>"-290","memo"=>"Balance adjusted for referring #{referred_user.email}. Payment for referral was successful."})
referring_user.subscription.save
if #subscription.product.handle == 'yearly'
schedule = ReferralSchedule.create(
type_of: "yearly discount",
referred_user_id: referred_user.id,
referring_user_id: referring_user.id,
time_to_execute: "30.41d"
)
ReferralScheduler::create_yearly_discounts_schedule(schedule)
end
end
head :ok
end
And the module handling the schedule creation:
module ReferralScheduler
def self.create_yearly_discounts_schedule(schedule)
referred_user = User.find(schedule.referred_user_id)
referring_user = User.find(schedule.referring_user_id)
# first_date is calculated to be the first date in the future, that the scheduler should run.
first_date = Time.now + (30.41-(DateTime.now - schedule.created_at.to_datetime).fdiv(30.41)%1*30.41).days
scheduler = Rufus::Scheduler.new
scheduler.every schedule.time_to_execute, first: first_date , last_at: (schedule.created_at+12.months) do
if referring_user && referred_user.subscription_status == :active
balance = referring_user.subscription.balance_in_cents
referring_user.subscription.adjustment({"amount_in_cents"=>"-290","memo"=>"Balance adjusted for referring #{referred_user.email}. Referral is still active."})
referring_user.subscription.save
end
end
scheduler.at (schedule.created_at+12.months) do
schedule.destroy!
end
end
end
And the initializer:
require 'rufus-scheduler'
ReferralSchedule.where(type_of: "yearly discount").each do |schedule|
ReferralScheduler::create_yearly_discounts_schedule(schedule)
end
I really hate using this method, as it doesn't seem very clean. It has also already screwed up my local server when I tried to run it at shorter intervals.
I also thought about using rake tasks and/or Grunt to perform this, but I have very limited knowledge on each of these, and I'm not sure if they will necessarily be easier/better solutions.
Any ideas?
One option might be to use the upcoming_renewal_notice webhook on Bob's subscription as a trigger to go see how many referrals he has made that are on annual plans, and apply any additional discounts.
https://docs.chargify.com/webhooks
upcoming_renewal_notice - A subscription set to renew within 3 days

Ruby on Rails - Percentage of Time More Than One Call is Active Per Hour

I have a RoR application that runs reports on a database of phone calls logged from a help desk. I've been asked to provide a report that shows the percentage of time, each hour, that more than one technician is on the phone. The database logs the call id, technician name, and call created at and end time in Y-M-D-H-M-S. Can anyone suggest a way I can do this? Thank you.
I don't see any problem at all from what I understand.
for each technician
look up the entries in the database and get the call created_at and end time
for each entry above
total_time += end_time.to_seconds - created_at_time.to_seconds
Find the difference between the starting time and the end time, you want to track the activity of a technician.
You get the %age of time this way. To get this for each hour, simply convert the time in hours and you are set to go.

Cache a complex calculation in Rails 3 model

I'm new to Ruby/Rails, so this is possibly (hopefully) a simple question that I just dont know the answer to.
I am implementing an accounting/billing system in Rails, and I'm trying to keep track of the running balance after each transaction in order to display it in the view as below:
Date Description Charges($) Credits($) Balance($)
Mar 2 Activity C $4.00 -$7.50
Feb 25 Payment for Jan $8.00 -$3.50
Feb 23 Activity B $1.50 -$11.50
Feb 20 Activity A $2.00 -$10.00
Each transaction (also known as line item) is stored in the database, with all the values above (Date, Description, Amount) except for the Balance. I can't store the balance for each transaction in the database because it may change if something happens to an earlier transaction (a payment that was posted subsequently failed later for example). So I need to calculate it on the fly for each line item, and the value for the Balance for a line item depends on the value for the line item before it (Balance = Balance of Prev Line Item + Amount for this Line Item, i.e.)
So here's my question. My current (inept) way of doing it is that in my LineItem model, I have a balance method which looks like :
def balance
prev_balance = 0
#get previous line items balance if it exists.
last_line_item = Billing::LineItem.get_last_line_item_for_a_ledger(self.issue_date,self.ledger_item_id)
if last_line_item
prev_balance = last_line_item.balance
.. some other stuff...
end
prev_balance + (-1*net_amount) # net_amount is the amount for the current line item
end
This is super costly and my view takes forever to load since I'm calculating the prev line item's balance again and again and again. Whats a better way to do this?
You're basically paying a price for not wanting to store the balance in each transaction. You could optimize your database with indices and use caches etc; but fundamentally you'll run into the problem that calculating a balance will take a long time, if you have lots of transactions.
Keep in mind that you'll continue to get new transactions, and your problem will thus get worse over time.
You could consider several design alternatives. First, like Douglas Lise mentioned, you could store the balance in each transaction. If an earlier dated transaction comes in, it means you may have to do an update of several transaction since that date. However, this has an upper-bound (depending on how "old" transactions you want to allow), so it has a reasonable worst-case behavior.
Alternatively, you can do a reconciliation step. Every month you "close the books" on transactions older than X weeks. After reconciliation you store the Balance you calculated. In def balance you now use your existing logic, but also refer to "balance as of the previous reconciliation". This again, provides a reasonable and predictable worst-case scenario.

Resources