How to validate data that was altered in before_save - ruby-on-rails

I have several validations that validate a Quote object. Upon validation, I have a before_save callback that calls an API and grabs more data, makes a few math computations and then saves the newly computed data in the database.
I don't want to trust the API response entirely so I need to validate the data I compute.
Please note that the API call in the before_save callback is dependent on the prior validations.
For example:
validates :subtotal, numericality: { greater_than_or_equal_to: 0 }
before_save :call_api_and_compute_tax
before_save :compute_grand_total
#I want to validate the tax and grand total numbers here to make sure something strange wasn't returned from the API.
I need to be able to throw a validation error if possible with something like:
errors.add :base, "Tax didn't calculate correctly."
How can I validate values that were computed in my before_save callbacks?

you can use after_validation
after_validation :call_api_and_compute_tax, :compute_grand_total
def call_api_and_compute_tax
return false if self.errors.any?
if ... # not true
errors.add :base, "Tax didn't calculate correctly."
end
end
....

Have you tried adding custom validation methods before save? I think this is a good approach to verify validation errors before calling save method
class Quote < ActiveRecord::Base
validate :api_and_compute_tax
private
def api_and_compute_tax
# ...API call and result parse
if api_with_wrong_response?
errors.add :base, "Tax didn't calculate correctly."
end
end
end
Then you should call it like
quote.save if quote.valid? # this will execute your custom validation

Related

Order/precedence of rails validation checks

I'm trying to get my head around the order/precedence with which Rails processes validation checks. Let me give an example.
In my model class I have these validation checks and custom validation methods:
class SupportSession < ApplicationRecord
# Check both dates are actually provided when user submits a form
validates :start_time, presence: true
validates :end_time, presence: true
# Check datetime format with validates_timeliness gem: https://github.com/adzap/validates_timeliness
validates_datetime :start_time
validates_datetime :end_time
# Custom method to ensure duration is within limits
def duration_restrictions
# Next line returns nil if uncommented so clearly no start and end dates data which should have been picked up by the first validation checks
# raise duration_mins.inspect # Returns nil
# Use same gem as above to calculate duration of a SupportSession
duration_mins = TimeDifference.between(start_time, end_time).in_minutes
if duration_mins == 0
errors[:base] << 'A session must last longer than 1 minute'
end
if duration_mins > 180
errors[:base] << 'A session must be shorter than 180 minutes (3 hours)'
end
end
The problem is that Rails doesn't seem to be processing the 'validates presence' or 'validates_datetime' checks first to make sure that the data is there in the first place for me to work with. I just get this error on the line where I calculate duration_mins (because there is no start_time and end_time provided:
undefined method `to_time' for nil:NilClass
Is there a reason for this or have I just run into a bug? Surely the validation checks should make sure that values for start_time and end_time are present, or do I have to manually check the values in all of my custom methods? That's not very DRY.
Thanks for taking a look.
Rails will run all validations in the order specified even if any validation gets failed. Probably you need to validate datetime only if the values are present.
You can do this in two ways,
Check for the presence of the value before validating,
validates_datetime :start_time, if: -> { start_time.present? }
validates_datetime :end_time, if: -> { end_time.present? }
Allows a nil or empty string value to be valid,
validates_datetime :start_time, allow_blank: true
validates_datetime :end_time, allow_blank: true
Simplest way, add this line right after def duration_restrictions
return if ([ start_time, end_time ].find(&:blank?))
Rails always validate custom method first.

Raising errors on failed custom validations in Rails

I wrote a custom validation to error out once a user attempts to withdraw more than the minimum allowed withdrawal amount. The validation fails as it should when the user's withdrawal is below the minimum allowed amount but the code continues running. It does not act like regular validations on model fields.
validate :minimum_withdrawal_amount, on: :create
validates :amount, numericality: {greater_than: 0}
def minimum_withdrawal_amount
if sum.nil? || sum < currency.min_withdraw_amount
errors.add :base, -> { I18n.t('activerecord.errors.models.withdraw.amount.min_withdraw_amount', currency: currency.key, amount: currency.min_withdraw_amount ) }
end
end
It goes ahead and validates the amount that comes after it. If that validation fails, it then errors out. I'll like my custom validation to act like the validation on amount. Hope I'm clear enough
Following replacement might work for you:
errors.add(:base, I18n.t('activerecord.errors.models.withdraw.amount.min_withdraw_amount', currency: currency.key, amount: currency.min_withdraw_amount ))

Rails form validates with custom method only if any value is provided

I have a conditional form field (which is shown by clicking a checkbox) which I want to validate with a custom method.
validates :number, length: { maximum: 20 }, if: :checksum_is_valid?
def checksum_is_valid?
if !Luhn.valid?(number)
errors.add(number, "is not valid.")
end
end
is my attempt. This technically works fine but the error is also shown, even if I don't enter any number at all (because the field is not mandatory). Any idea how I can check with a custom method, but only if the user provides any number at all.
Thanks
You could use validate instead of validates for custom validators and then move the check if there is a number present into the validator method:
validate :checksum_valid?
private
def checksum_valid?
if number.present?
errors.add(:number, "is not valid.") unless number_valid?
end
end
def number_valid?
number.length < 20 && Luhn.valid?(number)
end
You're mixing two things in your code, so let me help you understanding better what is happening.
1). conditional validation:
validates :number, length: { maximum: 20 }, if: :checksum_is_valid?
By this, Rails expects the model to implement method checksum_is_valid? which returns boolean value, based on which it will perform this validation. Something like would work:
def checksum_is_valid?
Luhn.valid?(number)
end
In case this method returns true, Rails would perform the validation of :number is not more than 20.
2). custom validation:
When using this custom method, you can build your own validation logic and error. So, if you have your method:
def checksum_is_valid?
if !Luhn.valid?(number)
errors.add(number, "is not valid.")
end
end
You should configure your validator with:
validate :checksum_valid?
Unfortunately, from your code is not clear what you would like to achieve (validate the number not to be more than 20, or perform the custom validator), but I hope what I've pointed will help you make the proper decision how to take that further.
Hope that helps!

Rails distinct validations

How to distinct which validator failed ?
I have multiple validations on the same field:
class User < ActiveRecord::Base
validates :name, presence: true, length: {minimum: 1, maximum: 20 }
validates_uniqueness_of :name
end
When I save the user as user.save - I want to distinct what failed.
if user.__not_valid_name_length__?
# name length wrong
# do smth 1
end
if **user.__not_valid_name_unique__?
# name is not unique
# do smth 2
end
I can access user.errors[:name] and see all error messages for the field.
But I don't want to rely on message text which can change.
Is there any way to know which validator failed?
A feature to return machine-parseable symbols instead of strings was committed to Rails almost a year ago, but it's still not available in the 4-x-stable branch. You can use it if you use the edge version, and it will be available in Rails 5.
Example:
user = User.new
user.valid?
user.errors.details[:name] # returns: [{error: :blank}, {error: :too_short}]
More info:
https://github.com/rails/rails/pull/18322
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb
There are no built-in callbacks for rails validation fails, for rails validation available callbacks are:
before_validation
after_validation
To learn more about callback, please read this call_back
Checking validation fail base on the message is no good approach. It may change in different scenarios i.e internationalization. Do it by defining the method for validation i.e
# check presence_of validation
def is_name_present?
self.name.present?
end
# check uniqueness validation
def is_name_uniqe?
User.where(name: self.name).count == 0
end
Use one which suit you best, I suggest use after_validation
after_validation :post_validatiom
def post_validation
unless is_name_present?
# do_someting
end
unless is_name_uniqe?
# do_something
end
end

Rails: validation fails with before_create method on datetime attribute

ApiKey.create!
Throws a validation error: expires_at can't be blank.
class ApiKey < ActiveRecord::Base
before_create :set_expires_at
validates :expires_at, presence: true
private
def set_expires_at
self.expires_at = Time.now.utc + 10.days
end
end
with attribute
t.datetime :expires_at
However, if the validation is removed the before_create method works on create.
Why is this? - This pattern works for other attributes, e.g. access_tokens (string), etc.
I would say because the before_create runs after the validation, maybe you want to replace before_create with before_validation
Note: If you leave the call back like that, it would set the expiry date whenever you run valid? or save or any active record method that fires the validation, You might want to limit this validation to the creation process only
before_validation :set_expires_at, on: :create
This will limit the function call only when the creation is run first time.

Resources