Including validation method in another method - ruby-on-rails

I have a validation method that has to verify values assigned in another method, how can i get it to recognise those values before validation? the pay_must_be_same_to_amount method needs some values from the create_items_from_readings method
class Invoice < ActiveRecord::Base
attr_accessible :approved_by, :due_date, :invoice_date, :reading_ids, :terms, :customer_id, :customer, :status, :reference_no, :payment_method, :amount, :payment_date
has_many :invoice_items, :dependent => :destroy
belongs_to :customer, :inverse_of => :invoices
validate :pay_must_be_same_to_amount
def create_item_from_readings
item = invoice_items.new
item.rate = customer.unit_cost
readings_in_this_period = customer.unbilled_readings.where('date_of_reading <= ?', invoice_date).order(:date_of_reading)
return nil if readings_in_this_period.empty?
self.reading_ids = readings_in_this_period.collect(&:id).join(',')
total_units = 0
readings_in_this_period.each do |reading|
total_units = total_units + reading.units_used1 + reading.units_used2 + reading.units_used3
end
item.amount = total_units * customer.unit_cost * customer.ct_ratio
item.tax_amount = (item.amount * Settings.vat) if customer.pays_vat
invoice_from_reading = readings_in_this_period.first.previous_reading
invoice_from_reading ||= readings_in_this_period.first
invoice_to_reading = readings_in_this_period.last
#Select Item description based on Phase type
if customer.phase_type == 'Single Phase'
item.description = "Electricity used from #{invoice_from_reading.date_of_reading.strftime('%d/%m/%Y')} with readings #{invoice_from_reading.reading1} to #{invoice_to_reading.date_of_reading.strftime('%d/%m/%Y')} with reading #{invoice_to_reading.reading1} - #{total_units.to_i} total units"
else
item.description = "Electricity used from #{invoice_from_reading.date_of_reading.strftime('%d/%m/%Y')} with readings, R1: #{invoice_from_reading.reading1}, R2: #{invoice_from_reading.reading2}, R3: #{invoice_from_reading.reading3} to #{invoice_to_reading.date_of_reading.strftime('%d/%m/%Y')} with readings, R1: #{invoice_to_reading.reading1}, R2:#{invoice_to_reading.reading2}, R3: # {invoice_to_reading.reading3}- #{total_units.to_i} total units"
end
end
end
and the validation method is below, it needs to compare the item.amount above to the amount in the class Invoice
def pay_must_be_same_to_amount
if item.amount < self.amount && item.amount != self.amount
self.errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end
end

A few comments: create_item_from_readings is way too complicated. I can't tell what it's supposed to do, but if you run it, I believe it will return a string (one of the two from the last if statement).
If all you need to do is compare item.amount to the invoice amount attribute, that's simple. You can use your validation method almost as you've written it, plus a few other methods as needed.
def item_amount
total_units * customer.unit_cost * customer.ct_ratio
end
def total_units
readings_in_this_period.inject(0) {|total,r| total + r.units_used1 + r.units_used2 + r.units_used3 }
end
def pay_must_be_same_to_amount
if item_amount != amount
errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end
The code for both of those supplementary methods is simply modified code from your longer method.
A good rule of practice is that if a method is longer than one line, and you can't tell what it's for by glancing at it, it's too long (this isn't always true, but it's worth considering for complicated methods).

The solution to the question is
def pay_must_be_same_to_amount
sum = 0
self.invoice_items.each do |invoice_item|
sum = sum + invoice_item.amount
end
if sum != self.amount
self.errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end

Related

Active Record: find records by ceratin condition

My goal is to find three doctors with more than 1 review and with average rating >= 4
At the moment I'm using this service
class RatingCounterService
def get_three_best_doctors
doctors = find_doctors_with_reviews
sorted_doctors = sort_doctors(doctors)
reversed_hash = reverse_hash_with_sorted_doctors(sorted_doctors)
three_doctors = get_first_three_doctors(reversed_hash)
end
private
def find_doctors_with_reviews
doctors_with_reviews = {}
Doctor.all.each do |doctor|
if doctor.reviews.count > 0 && doctor.average_rating >= 4
doctors_with_reviews[doctor] = doctor.average_rating
end
end
doctors_with_reviews
end
def sort_doctors(doctors)
doctors.sort_by { |doctor, rating| rating }
end
def reverse_hash_with_sorted_doctors(sorted_doctors)
reversed = sorted_doctors.reverse_each.to_h
end
def get_first_three_doctors(reversed_hash)
reversed_hash.first(3).to_h.keys
end
end
Which is very slow.
My Doctor model:
class Doctor < ApplicationRecord
has_many :reviews, dependent: :destroy
def average_rating
reviews.count == 0 ? 0 : reviews.average(:rating).round(2)
end
end
Review model:
class Review < ApplicationRecord
belongs_to :doctor
validates :rating, presence: true
end
I can find all doctors with more than 1 review with this request
doctors_with_reviews = Doctor.joins(:reviews).group('doctors.id').having('count(doctors.id) > 0')
But how can I find doctors with an average rating >= 4 and order them by the highest rating if the "average rating" is an instance method?
Thanks to this answer :highest_rated scope to order by average rating
My final solution is
Doctor.joins(:reviews).group('doctors.id').order('AVG(reviews.rating) DESC').limit(3)

Rails association scope by method with aggregate

I'm trying to retrieve association records that are dependent on their association records' attributes. Below are the (abridged) models.
class Holding
belongs_to :user
has_many :transactions
def amount
transactions.reduce(0) { |m, t| t.buy? ? m + t.amount : m - t.amount }
end
class << self
def without_empty
includes(:transactions).select { |h| h.amount.positive? }
end
end
class Transaction
belongs_to :holding
attributes :action, :amount
def buy?
action == ACTION_BUY
end
end
The problem is my without_empty method returns an array, which prevents me from using my pagination.
Is there a way to rewrite Holding#amount and Holding#without_empty to function more efficiently with ActiveRecord/SQL?
Here's what I ended up using:
def amount
transactions.sum("CASE WHEN action = '#{Transaction::ACTION_BUY}' THEN amount ELSE (amount * -1) END")END")
end
def without_empty
joins(:transactions).group(:id).having("SUM(CASE WHEN transactions.action = '#{Transaction::ACTION_BUY}' THEN transactions.amount ELSE (transactions.amount * -1) END) > 0")
end

method containing calculation - Rails 4

i just want to calculate the total sum of all active events that users have paid to attend. If you could advise me i could be grateful as i am very unsure. Many thanks
event.rb
has_many :payments
payment
belongs_to :event
in the event.rb i tried the below method but no success
def self.active_events
active_events = live_events.open_events
active_events.all.each do |event|
event.price * event.payments.count
end
end
You can do this simply in following way,
total = 0
Event.live_events.open_events.find_each { |e| total += e.price * e.payments.count }
In Event.rb place it in a method with meaningful name.
This will work for you.
def self.total_price_for_active_events
total = 0
Event.live_events.open_events.find_each { |e| total += e.price * e.payments.count }
total
end
Most optimized way
def self.total_price_for_active_events
Event.live_events.open_events.joins(:payments).sum("events.price")
end
You're off too a good start! Unfortunately, what you have there is only the beginning; you're generating an array that contains the total sum for each event. All that remains is to add them together:
def self.active_events
active_events = live_events.open_events
costs = active_events.all.each do |event|
event.price * event.payments.count
end
costs.reduce(0) do |sum,x|
sum + x
end
end
You could also get real fancy and simply use:
costs.reduce(0, :+)

Custom Rails Dashboard, how to optimize data retrieval to display in view?

I am making a custom dashboard for a school application that requires me to calculate some KPIs, the way am doing it right now is calling several class methods from the Opportunity class in the dashboard/index action from the controller, and storing each method result in a variable that is going to be used in a tile. So each variable is a different tile of the dashboard.
The methods belong to the Opportunity class shown below:
class Opportunity < ApplicationRecord
belongs_to :organization
belongs_to :opportunity_status
has_many :tasks, dependent: :destroy
has_many :opportunity_status_logs, dependent: :destroy
before_create :create_status_log
after_update :create_status_log, if: :opportunity_status_id_changed?
validates :name, :description, :revenue, :opportunity_status_id, :closing_date, presence: true
validates :name, :description, format: { with: /\A[[:alpha:]a-zA-Z0-9ñÑ#()\-.,\s]+\z/ }
validates :revenue, numericality: true
validates :closing_date, inclusion: { in: (Time.zone.today..Time.zone.today+5.years) }
def create_status_log
OpportunityStatusLog.create(opportunity_id: self.id, opportunity_status_id: self.opportunity_status_id)
end
def status_updated_by(user)
#status_log = self.opportunity_status_logs.last
#status_log.user_id = user.id
#status_log.save!
end
def self.actives
self.where.not(opportunity_status_id: [11,12])
end
def self.won
self.where(opportunity_status_id: 11)
end
def self.lost
self.where(opportunity_status_id: 12)
end
def self.average_revenue
self.won.average(:revenue)
end
def self.minimum_revenue
self.won.minimum(:revenue)
end
def self.maximum_revenue
self.won.maximum(:revenue)
end
def self.filter_by_status(status_id)
self.where(opportunity_status: status_id)
end
def self.relative_percentage(item_amount, total)
item_amount * 100 / total
end
def self.conversion_rate
self.won.count / self.all.count.to_f * 100
end
def self.potential_revenue
self.actives.sum(:revenue)
end
end
and this is the way the controller is structured:
class DashboardController < ApplicationController
before_action :authenticate_user!
def index
#opportunities = Opportunity.includes(:opportunity_status).all
#actives = Opportunity.actives.count
#won = Opportunity.won.count
#lost = Opportunity.lost.count
#average_revenue = Opportunity.average_revenue
#minimum_revenue = Opportunity.minimum_revenue
#maximum_revenue = Opportunity.maximum_revenue
#in_appreciation = Opportunity.filter_by_status(6).count
#in_value_proposition = Opportunity.filter_by_status(7).count
#in_management_analysis = Opportunity.filter_by_status(8).count
#in_proposal = Opportunity.filter_by_status(9).count
#in_review = Opportunity.filter_by_status(10).count
#app_perc = Opportunity.relative_percentage(#in_appreciation, #opportunities.count)
#vp_perc = Opportunity.relative_percentage(#in_value_proposition, #opportunities.count)
#ma_perc = Opportunity.relative_percentage(#in_management_analysis, #opportunities.count)
#pp_perc = Opportunity.relative_percentage(#in_proposal, #opportunities.count)
#rw_perc = Opportunity.relative_percentage(#in_review, #opportunities.count)
#conversion_rate = '%.2f' % [Opportunity.conversion_rate]
#potential_revenue = Opportunity.potential_revenue
end
end
Even though it works as expected, it looks like the controller is a bit too fat and I feel that with the current approach if the app scales it will be very slow due to the amount of queries that are being done. So, is there a way to refactor this in order to optimize the data retrieval and the displaying of the KPIs?
Thanks in advance
You can try implementing Facade Pattern in Rails. It will make your controller skinny but on the query part you will still be needing to make those queries, there is no way to skip that.
You can try to optimize db by adding index and creating sql views in future when you get performance lag, at this time it will be like premature optimization

How to get value from select field in controller?

I have a problem with get value from :days new form.
new.html.haml
= f.select :days, [['1 month', 30], ['2 months', 60], ['3 months', 90]]
controller
def create
#bought_detail = BoughtDetail.new(bought_detail_params)
#bought_detail.entry_type = #entry_type
#bought_detail.person = current_person
if #bought_detail.entry_type.kind == 'Karnet'
#bought_detail.cost = #bought_detail.entry_type.price * (days / 30).floor
else
#bought_detail.cost = #bought_detail.entry_type.price
end
end
private
def bought_detail_params
params.require(:bought_detail).permit(:bought_data, :start_on, :end_on, :entry_type_id, :days, :person_id, :credit_card, :card_code)
end
Model
belongs_to :entry_type
belongs_to :person
before_save :set_bought_data, :set_end_on, :set_start_on
attr_accessor :credit_card, :card_code, :days
def set_bought_data
self.bought_data = Date.today
end
def set_start_on
self.start_on = bought_data if start_on.nil?
end
def set_end_on
days = "7" if days.nil?
self.end_on = Date.today + days.to_i
end
When I fill new form and click submit I get this error:
undefined local variable or method `days'
I want to deliver value from select field e.g. when I choose 2 months, value of the day will be 60 and I will can get calculate #bought_detail.cost. How to do this?
Thanks in advance!
In controller you're trying to access undefined variable days, change to accessing the model:
#bought_detail.cost = #bought_detail.entry_type.price * (#bought_detail.days.last / 30).floor
In model change to:
self.days = "7" if days.nil?
because otherwise you'll be assigning to local variable, not the attribute.
In your controller's create method. Where are you getting the days from?
#bought_detail.cost = #bought_detail.entry_type.price * (days.last / 30).floor
This bit specifically: days.last / 30. Ruby can't find days method or local variable. I guess you want to access it from params[:your_model][:days]?

Resources