rails 4 refactor factorygirl creates inaccurate data - ruby-on-rails

I have been battling a major refactor to slim down a payments controller and could use a hand. Step one I am trying to fix my factories. Right now all of the factories work great on their own, but when I try to build associations the FactoryGirl.create(:job, :purchased_with_coupon) it will setup the association correctly on the coupon but not the payment. This means that the price paid is always is always 1. I just noticed this which you can see the other section commented out. Before I start tackling the bloated controller I need to figure this out for my tests. Thoughts?
Factories
FactoryGirl.define do
factory :job do
category
company
title { FFaker::Company.position }
location { "#{FFaker::Address.city}, #{FFaker::AddressUS.state}" }
language_list { [FFaker::Lorem.word] }
short_description { FFaker::Lorem.sentence }
description { FFaker::HTMLIpsum.body }
application_process { "Please email #{FFaker::Internet.email} about the position." }
trait :featured do |job|
job.is_featured true
end
trait :reviewed do |job|
job.reviewed_at { Time.now }
end
trait :purchased do |job|
job.reviewed_at { Time.now }
job.start_at { Time.now }
job.end_at { AppConfig.product['settings']['job_active_for_day_num'].day.from_now }
job.paid_at { Time.now }
payments { |j| [j.association(:payment)] }
end
trait :purchased_with_coupon do |job|
job.reviewed_at { Time.now }
job.start_at { Time.now }
job.end_at { AppConfig.product['settings']['job_active_for_day_num'].day.from_now }
job.paid_at { Time.now }
association :coupon, factory: :coupon
payments { |j| [j.association(:payment)] }
end
trait :expired do |job|
start_at = (200..500).to_a.sample.days.ago
job.reviewed_at { start_at }
job.start_at { start_at }
job.end_at { |j| j.start_at + AppConfig.product['settings']['job_active_for_day_num'].days }
job.paid_at { start_at }
payments { |j| [j.association(:payment)] }
end
end
end
FactoryGirl.define do
factory :payment do
job
# price_paid { rand(100..150) }
price_paid { 1 }
stripe_customer_token { (0...50).map { (65 + rand(26)).chr }.join }
end
end
FactoryGirl.define do
factory :coupon do
code { rand(25**10) }
percent_discount { rand(100**1) }
start_at { 2.days.ago }
end_at { 30.day.from_now }
trait :executed do |c|
association :job, factory: [:job, :purchased]
c.executed_at { Time.now }
end
end
end
Models
class Job < ActiveRecord::Base
acts_as_paranoid
strip_attributes
acts_as_taggable
acts_as_taggable_on :languages
belongs_to :company
before_validation :find_company
belongs_to :category
has_one :coupon
has_many :payments
before_create :create_slug, :set_price
after_create :update_vanity_url
accepts_attachments_for :company
accepts_nested_attributes_for :company
accepts_nested_attributes_for :coupon
accepts_nested_attributes_for :payments
validates :title,
:location,
:short_description,
presence: true,
format: { with: /\A[\w\d .,:-#]+\z/, message: :bad_format }
validates :application_process,
presence: true,
format: { with: %r{\A[\w\d .,:/#&=?-]+\z}, message: :bad_format }
validates :title, length: { minimum: 10, maximum: 45 }
validates :location, length: { minimum: 10, maximum: 95 }
validates :short_description, length: { minimum: 10, maximum: 245 }
validates :application_process, length: { minimum: 10, maximum: 95 }
validates :description,
:category_id,
:language_list,
presence: true
validates :reviewed_at,
:start_at,
:end_at,
:paid_at,
date: { allow_blank: true }
validates :start_at, date: { before: :end_at, message: :start_at_before_end_at }, if: proc { start_at? }
validates :end_at, date: { after: :start_at, message: :end_at_after_start_at }, if: proc { end_at? }
scope :active, -> { where.not(reviewed_at: nil, paid_at: nil).where('end_at >= ?', Date.today) }
def expired?
end_at.present? && end_at < Date.today
end
def reviewed?
reviewed_at.present?
end
def paid_for?
reviewed? && paid_at.present?
end
def active?
reviewed? && paid_at.present? && end_at <= Date.today
end
private
def set_price
self.price = AppConfig.product['settings']['job_base_price']
end
def create_slug
self.slug = title.downcase.parameterize
end
def update_vanity_url
self.vanity_url = '/jobs/' + company.slug + '/' + slug + '/' + id.to_s + '/'
save
end
def find_company
existing_company = Company.where(email: company.email) if company
self.company = existing_company.first if existing_company.count > 0
end
end
class Coupon < ActiveRecord::Base
acts_as_paranoid
strip_attributes
belongs_to :job
validates :start_at, date: { before: :end_at }
validates :executed_at, date: { allow_blank: true }
validates_presence_of :job, if: proc { executed_at? }
validates_presence_of :executed_at, if: :job
validates :code,
presence: true,
length: { minimum: 10, maximum: 19 },
uniqueness: { case_sensitive: false },
numericality: { only_integer: true }
validates :percent_discount,
inclusion: { in: 1..100 },
length: { minimum: 1, maximum: 3 },
numericality: { only_integer: true },
presence: true
scope :active, -> { where('start_at < ? AND end_at > ? AND executed_at IS ?', Date.today, Date.today, nil) }
def active?
start_at < Date.today && end_at > Date.today && executed_at.nil?
end
def executed?
job_id.present?
end
end
class Payment < ActiveRecord::Base
belongs_to :job
belongs_to :coupon
validates_presence_of :job
validate :coupon_must_be_active
before_create :net_price
Numeric.include CoreExtensions::Numeric::Percentage
attr_accessor :coupon_code
def coupon_code=(code)
#coupon = Coupon.find_by_code(code)
end
def net_price
return job.price unless #coupon
job.price = #coupon.percent_discount.percent_of(job.price)
self.coupon = #coupon
end
private
def coupon_must_be_active
if #coupon
errors[:coupon] << I18n.t('flash_messages.coupons.id.inactive') unless #coupon.active?
elsif #coupon_code.present?
errors[:coupon_code] << I18n.t('flash_messages.coupons.id.not_found')
end
end
end

It looks like the problem is that there is logic outside of your models that is updating the price_paid column on your Payment, and possibly setting the coupon_id on it as well.
So I would recommend duplicating any extra logic that might be coming from your controllers, service classes, etc. into an after(:create) callback on your factory.
trait :purchased_with_coupon do
# ...other attributes...
association :coupon
after(:create) do |job, evaulator|
discount_value = 100 - job.coupon.percent_discount) / 100.0
calculated_price_paid = job.price * discount_value
create(:payment, price_paid: price_paid, job: job, coupon: coupon)
end
end
Now ultimately, that code belongs in some kind of abstraction, such as a service class that can easily be tested (and used in other tests). However, you mentioned you are getting started on a refactor and want passing tests. I think this is a reasonable compromise until you're ready to abstract it. Ultimately, I would do something like this:
class CreatePaymentWithCoupon
attr_reader :job
def initialize(job)
#job = job
end
def call
job.payments.create(coupon: job.coupon, price_paid: discounted_price)
end
private
def discounted_price
discount_value = (100 - job.coupon.percent_discount) / 100.0
job.price * discount_value
end
end
Then, in your specs:
it "calculates discounted price" do
coupon = create(:coupon, percent_discount: 25)
job = create(:job, :purchased_with_coupon, price: 100)
CreatePaymentWithCoupon.new(job).call
expect(job.payments.first.price_paid).to eq(75.0)
end

Related

undefined method `__metadata' for #<Participant:0x00000001076da378> with rails 6 / mongoid

I have the follow code that is working in rails 5. Updagrate to 6 I get the error undefined method `__metadata'.
Here's the problematic code
*
def nature
self.__metadata.key.to_s.singularize.to_sym #
end
*
Have try to use method but it doesn't return what it does in rails 5 / mongoid. Mongoid version is '~> 7.0'
Complete class code
# Participant model class definition
class Participant
include Mongoid::Document
include Mongoid::Timestamps
include DryValidation
field :address
field :identifier
field :name
field :birthdate, type: Date
field :sex
field :profession
field :phone
field :email
field :ownership_percentage
field :contribution_amount
field :category
field :group
field :registered_on, type: Date
field :retired, type: Boolean
field :retired_on, type: Date
field :committee
# Callbacks
before_save :generate_identifier
# Relations
embedded_in :book, inverse_of: :shareholders
embedded_in :book, inverse_of: :directors
embedded_in :book, inverse_of: :employees
embedded_in :book, inverse_of: :committee_members
embeds_many :participant_files
accepts_nested_attributes_for :participant_files, allow_destroy: true
#Validations
validates :name, presence: true
validates :email, allow_blank: true, format: { with: /\A\S+#\S+\.\S+\z/i }
validates :registered_on, presence: true, non_existent_date: true
validates :birthdate, non_existent_date: true
validates :retired_on, non_existent_date: true
validate :registered_on_date
def self.options_for(field_name)
case field_name.to_sym
when :category then [nil, :founders, :actives, :participants]
when :sex then [nil, :male, :female]
when :group then [nil, :legal, :accounting, :human_resources, :consumer, :employee,
:management_and_administration, :communication_and_marketing,
:ethic_and_gouvernance, :other]
else []
end
end
def self.ordered
# This should be as simple as .order_by(:retired_on.desc, :registered_on.asc)
# but the registered_on parameters is never ordered correctly so I had to do this ugly thing :(
self.all.sort_by{ |a| [ (a.retired_on ? a.retired_on.strftime('%Y%m%d') : 0), (a.registered_on ? a.registered_on.strftime('%Y%m%d') : 0) ].join }
end
def self.ordered_by_name
participants = self.active.sort_by{ |p| p.name.downcase }
participants += self.inactive.sort_by{ |p| p.name.downcase }
participants
end
def self.active
now = Time.now.strftime('%Y%m%d')
self.all.select do |a|
if a.registered_on
if a.retired_on
a.retired_on.strftime('%Y%m%d') >= now && a.registered_on.strftime('%Y%m%d') <= now
else
a.registered_on.strftime('%Y%m%d') <= now
end
end
end
end
def self.inactive
now = Time.now.strftime('%Y%m%d')
self.all.select do|a|
(a.retired_on && a.retired_on.strftime('%Y%m%d') < now) ||
(a.registered_on && a.registered_on.strftime('%Y%m%d') > now)
end
end
def book
self._parent
end
def committee_member?
self.nature == :committee_member
end
def director?
self.nature == :director
end
def employee?
self.nature == :employee
end
def nature
self.__metadata.key.to_s.singularize.to_sym #
end
def active?
!retired?
end
def retired?
self.retired_on && self.retired_on <= Time.zone.today
end
def shareholder?
self.nature == :shareholder
end
def securities
self.book.transactions.any_of({from: self.id}, {to: self.id}).asc(:transacted_on)
end
def save_files
self.participant_files.each do |pf|
pf.save
end
delete_objects_without_file
end
def has_shares?
book.share_categories.each do |sc|
return true if total_shares(sc) > 0
end
false
end
def total_shares(share_category)
total = 0
securities.each do |s|
if s.share_category == share_category
if s.nature == 'issued' or (s.nature == 'transfered' and self.id.to_s == s.to.to_s)
total += s.quantity if s.quantity
elsif s.nature == 'repurchased' or (s.nature == 'transfered' and self.id.to_s == s.from.to_s)
total -= s.quantity if s.quantity
end
end
end
total
end
def share_class_percentage(sc)
book.share_class_quantity(sc) > 0 ? self.total_shares(sc)/book.share_class_quantity(sc).to_f*100 : 0
end
def acceptance_documents
self.book.documents.select{|document| document.participant_id == self.id && document.nature == 'dir_accept'}
end
def resignation_documents
self.book.documents.select{|document| document.participant_id == self.id && document.nature == 'dir_resig'}
end
private
def existing_identifier?
participant_type = self.__metadata.key.to_sym
identifiers = book.send(participant_type).map{ |p| p.identifier if p.id != self.id }.compact
identifiers.include? self.identifier
end
def generate_identifier
self.identifier = self.name.parameterize if self.identifier.blank?
i = 1
while existing_identifier?
self.identifier = "#{self.identifier}-#{i}"
i += 1
end
end
def registered_on_date
unless registered_on.nil? || retired_on.nil?
if registered_on > retired_on
errors.add(:registered_on, I18n.t("mongoid.errors.models.participant.attributes.registered_on.greater_than_retired_on"))
end
end
end
def delete_objects_without_file
self.participant_files.each do |pf|
pf.delete if pf.pdf_file.file.nil?
end
end
end```

Rails Order by Scope with Draper/Decorate from other model

So I have a pretty complex model that is using a scope to establish what is considered online and what is considered offline. Then in my admin module I'm displaying all of the available devices. What I'm looking to do is then order by those that are currently online.
So the model looks like:
class Device < ActiveRecord::Base
include Tokenizer
belongs_to :user
belongs_to :organization
belongs_to :former_user, class_name: 'User', foreign_key: 'former_user_id'
belongs_to :order
belongs_to :replaced_by_device, class_name: 'Device', foreign_key: 'replaced_by_device_id'
has_and_belongs_to_many :user_clients, join_table: :user_clients_devices
has_many :user_client_speed_tests, through: :user_clients
validates :hardware_token, uniqueness: true, presence: true, length: { maximum: 50 }
validates :mac, mac_address: true, allow_blank: false, allow_nil: true
before_validation :generate_hardware_token, on: :create
before_validation :assign_organization_id_if_missing
validate :existence_of_user_id, if: :user_id?
validate :existence_of_organization_id, if: :organization_id?
validates_numericality_of :user_id, :organization_id, allow_nil: true, greater_than_or_equal_to: 0
alias_attribute :name, :mac
scope :with_macs, -> { where("mac IS NOT NULL AND hardware_mac <> ''") }
scope :without_macs, -> { where("mac IS NULL OR hardware_mac = ''") }
scope :with_old_macs, -> { where("mac LIKE :prefix", prefix: "C0%") }
scope :with_new_macs, -> { where("mac LIKE :prefix", prefix: "A%") }
scope :without_user, -> { where(user_id: nil) }
scope :with_user, -> { where.not(user_id: nil) }
scope :online, -> { where("last_seen_at > ?", 1.hour.ago) }
scope :offline, -> { where.not(id: online.ids) }
scope :installed_online, -> { installed.online }
scope :installed_offline, -> { installed.where.not(id: installed_online.ids) }
enum status: [ :operational, :replaced ]
after_save :set_provisioned_if_needed
has_paper_trail
ransacker :mac_address_presence, formatter: proc{ |value|
value.eql?('present') ? with_macs.ids : without_macs.ids
}, splat_params: true do |parent| parent.table[:id] end
ransacker :mac_address_type, formatter: proc{ |value|
value.eql?('old') ? with_old_macs.ids : with_new_macs.ids
}, splat_params: true do |parent| parent.table[:id] end
ransacker :organization_presence, formatter: proc{ |value|
value.eql?('present') ? with_organization.ids : without_organization.ids
}, splat_params: true do |parent| parent.table[:id] end
ransacker :installation_status, formatter: proc{ |value|
case value
when 'installed' then installed.ids
when 'not_installed' then not_installed.ids
when 'not_assigned' then not_assigned.ids
end
}, splat_params: true do |parent| parent.table[:id] end
ransacker :connection_status, formatter: proc{ |value|
data = value.eql?('online') ? online.ids : offline.ids
data.any? ? data : nil
}, splat_params: true do |parent| parent.table[:id] end
ransacker :wifi_signal_strength, formatter: proc{ |value|
data = case value
when 'borderline' then with_borderline_signal_strength.ids
when 'bad' then with_bad_signal_strength.ids
when 'ok' then with_ok_signal_strength.ids
when 'good' then with_good_signal_strength.ids
else with_great_signal_strength.ids end
data.any? ? data : nil
}, splat_params: true do |parent| parent.table[:id] end
def update_status(new_status)
update!(status: new_status, status_last_changed_at: Time.now.utc)
end
def can_replace_hw?
operational? && (order.nil? || (order.present? && order.completed?))
end
def last_user_client
user_clients.last
end
def last_user_client_speed_test
last_user_client.last_speed_test if last_user_client.present?
end
def speed_tests
user_client_speed_tests
end
def has_last_user_client?
last_user_client.present?
end
def has_been_seen?
has_last_user_client? && last_user_client.last_seen_at.present?
end
def offline?
if has_been_seen?
last_user_client.last_seen_at < 1.hour.ago
end
end
def online?
if has_been_seen?
last_user_client.last_seen_at > 1.hour.ago
end
end
def connection_status_history
last_seen_history = last_seen_histories.where('last_seen_at > ?', 2.weeks.ago).order(:last_seen_at).to_a
status_history = []
while last_seen_history.present?
next_last_seen = last_seen_history.shift
status_history << {
status: "Online",
timestamp: next_last_seen.last_seen_at.in_time_zone(Time.now.zone)
}
if (last_seen_history.first&.last_seen_at || Time.current) - status_history.last[:timestamp] > 1.hour
status_history << {
status: "Offline",
timestamp: status_history.last[:timestamp].in_time_zone(Time.now.zone) + 1.hour
}
end
end
status_history
end
end
Then in my admin view I have an input I'm referencing with:
= f.input :user_device_ids, label: false, as: :select, multiple: true, collection: #organization.available_devices.decorate
So from the organization I'm wanting to order by the online Devices. I thought I could do something like #organization.available_devices.order(online).decorate. That clearly fails because online is a scope of Devices not from Organization. So if I do something like #organization.available_devices.order(Device.online).decorate I get no errors. That seems wrong/sloppy.
How do I accurately display for the collection the online devices in the different model?
Two things came up looking at your code sample:
You have belongs_to :order which could easily be confused with ActiveRecord's order method: https://apidock.com/rails/ActiveRecord/QueryMethods/order
If you temporarily remove that belongs_to :order and try #organization.available_devices.order(:last_seen_at, :desc).decorate
things should work as expected.
Hope that this helps.

Rails Create new active record with association value passed in params

I have 2 rails models which look like this
class Physician < UserProfile
has_many :state_licenses, inverse_of: :physician, autosave: true, dependent: :destroy
validates :state_licenses, :length => { :minimum => 1, message: "Please select at-least one state license"}
class StateLicense < ApplicationRecord
include RailsAdminPhysicianDependencyConcern
belongs_to :physician, inverse_of: :state_licenses
belongs_to :state, optional: true
attr_accessor :client_id
validates :state, presence: { message: I18n.t("errors.choose_one", field: 'state') }
#validates :license_number, presence: { message: I18n.t("errors.blank") }
def name
return "" unless state
"#{state.try(:name)}"
end
end
In my controller, I am using the code below to create a new Physician record with a bunch of state licenses but for some reason, the state licenses I pass to the create function never make it to the Physician model
def create
physician = nil
ActiveRecord::Base.transaction do
state_licenses = params["state_licenses"]
state_licenses_For_Association = []
if (state_licenses != nil)
state_licenses.each do |state_license|
sl = {}
sl[:state_id] = state_license
state_licenses_For_Association.push(sl)
end
end
physician = Physician.create(params.permit(:first_name, :last_name, :title, :residency_board_status, :residency_specialty_id, :state_licenses => state_licenses_For_Association))
user_record = nil
super do |user|
user_record = user
user.errors.delete(:user_profile)
physician.errors.messages.each { |field, messages| messages.each {|message| user.errors.add(field, message)} }
end
raise ActiveRecord::Rollback unless user_record.persisted? && physician.persisted?
end
AdminNotificationsMailer.physician_signed_up(physician).deliver_now rescue nil
end
What am I doing wrong?
Try changing this:
physician = Physician.create(params.permit(:first_name, :last_name, :title, :residency_board_status, :residency_specialty_id, :state_licenses => state_licenses_For_Association))
to this:
physician = Physician.create(params.permit(:first_name, :last_name, :title, :residency_board_status, :residency_specialty_id).merge(state_licenses: state_licenses_For_Association)) # note the .merge call

shoulda-matchers fail when attribute saved in a model's callback

class StudentPiggyBank < ActiveRecord::Base
PERIODS = [['tydzień', :week], ['miesiąc', :month], ['trzy miesiące', :three_months]]
RATES_MULTIPLIERS = {week: 1, month: 1.5, three_months: 2}
INTEREST_RATE_PRECISION = 2
before_validation :set_interest_rate
validates :completion_date, presence: true
validates :balance, numericality: {greater_than_or_equal_to: 0,
message: I18n.t('errors.messages.negative_piggy_bank_balance')}
validates :interest_rate, numericality: {greater_than_or_equal_to: 0,
message: I18n.t('errors.messages.negative_interest_rate')}
def self.date_from_param(period_param)
case period_param
when 'week'
1.week.from_now
when 'month'
1.month.from_now
when 'three_months'
3.months.from_now
end
end
protected
def set_interest_rate
num_of_days = completion_date - Date.today
if num_of_days >= 90
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:three_months], INTEREST_RATE_PRECISION)
elsif num_of_days >= 30
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:month], INTEREST_RATE_PRECISION)
else
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:week], INTEREST_RATE_PRECISION)
end
end
end
This code works. However, when testing with shoulda-matchers
describe StudentPiggyBank do
it { should validate_numericality_of(:interest_rate).is_greater_than_or_equal_to(0) }
it { should validate_numericality_of(:balance).is_greater_than_or_equal_to(0) }
end
I get errors for the line num_of_days = completion_date - Date.today:
NoMethodError:
undefined method `-' for nil:NilClass
Why completion_date is nil?
Well, it will basically do a described_class.new, so you won't have a completion_date. You can fix it like this:
describe StudentPiggyBank do
context 'with a completion date' do
before { subject.completion_date = 7.days.from_now }
it { should validate_numericality_of(:interest_rate).is_greater_than_or_equal_to(0) }
end
end

Passing arguments to Faker for my method in Rails?

I have a model called Booking, that should calculate the total from several numbers (amount, deposit, and fee are all added together). I'm having trouble getting these arguments to be seen in Faker.
it "should calculate the total" do
myvar = FactoryGirl.create(:booking, :amount => 900, :deposit => 20, :fee => 8)
myvar.totalamount.should == 928
end
And here's my method:
class Booking < ActiveRecord::Base
validates :to, :from, :amount, presence: true
def totalamount(amount,deposit,fee)
total = (amount + deposit + fee)
return total
end
end
The error message: "wrong number of arguments (0 for 3)"
However, when I do a puts myvar.deposit, it returns the value I gave it - 20.
What am I doing wrong?
Edit: Here is my Factory build for Booking:
FactoryGirl.define do
factory :booking do |b|
b.from { Faker::Lorem.sentence(word_count=3) }
b.to { Faker::Lorem.sentence(word_count=3) }
b.amount { Faker::Number.digit }
end
end
class Booking < ActiveRecord::Base
validates :to, :from, :amount, presence: true
def totalamount
total = (amount + deposit + fee)
return total
end
end
Just had to remove the 3 required attributes after 'totalamount'.

Resources