Rails Merging Child Errors with Parent - ruby-on-rails

I am using stripe to setup a payment system. I am having an issue with my credit_card model displaying the stripe errors in my user model, as the first credit card is submitted as a child of the user. Also, I am using Devise for my users auth.
class CreditCard < ActiveRecord::Base
def create_stripe_credit_card
customer = Stripe::Customer.retrieve(self.user.stripe_customer_id)
credit_card = customer.cards.create(:card => self.stripe_token)
rescue Stripe::CardError => e
logger.error "Stripe Error: " + e.message
errors.add(:base, "#{e.message}")
false
end
end
class RegistrationsController < Devise::RegistrationsController
def create
build_resource(sign_up_params)
if resource.save
# User created
else
clean_up_passwords resource
return render :status => 400, :json => resource.errors
end
end
end
After some googling I came upon this SO question
Ruby on Rails: how to get error messages from a child resource displayed?
but it seems that I would have to put a validator in my CreditCard model to trigger the errors in my User model, and since the only way to get this error raised is to try it, not sure how to put it into a validator.
Any help would be greatly appreciated.

Related

Passing Gibbon (Mailchimp) error from service to controller

I am using Mailchimp (via the Gibbon gem) to add email addresses to my Mailchimp mailing list, and I want to handle any errors that are returned by Mailchimp and display them in my view.
Here is my Pages controller:
class PagesController < ApplicationController
def subscribe
email = subscriber_params[:email]
if email.empty?
flash[:error] = 'Please provide an email.'
redirect_to root_path
else
subscriber = Mailchimp.new.upsert(email)
if subscriber
flash[:success] = 'You\'re in!'
redirect_to root_path(subscribed: :true)
else
# Return error coming from Mailchimp (i.e. Gibbon::MailChimpError)
end
end
end
end
And here is the app > services > mailchimp.rb file I set up to separate out the Mailchimp logic:
class Mailchimp
def initialize
#gibbon = Gibbon::Request.new(api_key: Rails.application.credentials.mailchimp[:api_key])
#list_id = Rails.application.credentials.mailchimp[:list_id]
end
def upsert(email)
begin
#gibbon.lists(#list_id).members.create(
body: {
email_address: email,
status: "subscribed"
}
)
rescue Gibbon::MailChimpError => e #This is at the bottom of the Gibbon README
raise e.detail
end
end
end
What I'm trying to figure out is how to return/send Gibbon::MailChimpError back to my Pages#subscribe action. I see it being outputted as a RuntimeError in my console, but I'm not sure the right way to access/pass it along.
And please let me know if there's a better practice for this kind of implementation.
You could move the begin/rescue block to the subscribe action inside your controller to handle the error from there, or even better, you can use rescue_from in your controller like this:
class PagesController < ApplicationController
rescue_from Gibbon::MailChimpError do |e|
# Handle the exception however you want
end
def subscribe
# ....
end
end

Rails - How to handle record create with has_many association error

I am running into a problem when trying to handle a #create request on a model with a has_many association where one of the passed in IDs does not belong to an existing record.
Test request:
post authors_path, params: { book_ids: [-1] }
Controller method:
def create
#author= Author.create params
end
Model:
class Author
has_many :books
end
This results in an ActiveRecord::RecordNotFound error being raised.
The problem is as follows:
I am already rescuing from an ActiveRecord::RecordNotFound error and responding with 404 Record Not Found in my ApplicationController because such an error typically arises when a user is attempting to GET, PATCH, PUT, or DELETE a record that does not exist, e.g., get author_path(-1). I would prefer to avoid moving the rescue clause onto the #show, #create, etc methods because I have a lot of controllers, resulting in a lot of duplicate code.
I want to keep my record and association creations atomic and this seems to be the best way to do it, but I also want to respond with a 400 Bad Request when the situation described above occurs. What would be the best way to handle such a situation?
UPDATE
After some more research, I wrote a quick custom validation that validates a record exists for all passed in book_ids
class Author < ApplicationRecord
validate :books_exist
def books_exist
return if book_ids.blank?
Book.find book_ids
rescue ActiveRecord::RecordNotFound => e
errors.add e.message
end
end
This doesn't seem to work though as even instantiating a new Author instance without saving it to the database throws an ActiveRecord::RecordNotFound error:
> Author.new(association_ids: [-1])
Book Load (2.3ms) SELECT `books`.* FROM `books` WHERE `books`.`id` = -1
ActiveRecord::RecordNotFound: Couldn't find Book with 'id'=[-1]
from ...
The issue seems to be that ActiveRecord attempts to find a record for the passed in book_id before any validation occurs. Is there any way to rescue this error? It seems like there's not much of a workaround for this particular issue.
The two solutions that were suggest to me outside of StackOverflow are as follows:
Rescue the error in each controller action
class AuthorsController
def create
#author = Author.create(params)
render json: #author
rescue ActiveRecord::RecordNotFound => e
render json_error_message
end
end
Create a generic action in the ApplicationController
class ApplicationController
def create(model)
instance_variable_set("##(model.class.name}", model.create(params)
rescue ActiveRecord::RecordNotFound => e
render json_error_message
end
end
class AuthorsController
def create
super(Author)
end
end

StandardError redirect to page

I was handed a project that another developer worked on, without leaving any documentation behind. The code fetches some purchases from a shopping website, looks for a price and notifies the user.
The app may encounter errors like "no results found" and then I raise a standarderror.
I want to redirect the user to the error page and notify them about it but I can't do that because it isn't a controller, so the redirect_to option doesn't work.
services/purchase_checker.rb is called once an hour:
def call
user.transaction do
store_purchase
if better_purchase?
update_purchase
end
end
rescue MyError=> e
store_error(e)
end
def store_error(error)
user.check_errors.create!(error_type: error.class.name, message: error.message)
end
services/my_error.rb:
class MyError< StandardError
def initialize(error_type, error_message)
super(error_message)
#error_type = error_type
end
attr_reader :error_type
end
services/purchase_fetcher.rb:
def parse_result_page
raise purchase_form_page.error if purchase_form_page.error.present?
offer = purchase_page.map{|proposal_section|
propose(proposal_section, purchase) }
.min_by(&:price)
offer or raise MyError.new("No results", "No results could be found")
end
you should create another err class, eg NotFoundError:
offer or raise NotFoundError.new("No results", "No results could be found")
then in your controller:
begin
parse_result_page
rescue NotFoundError => e
redirect_to err_page, :notice => e.message
end
Since this is running in a job, the best way to notify the user would be by email, or some other async notification method. When an error is detected, an email is sent.
If that's not an option for some reason, you can check if a user has check_errors in any relevant controllers. Looking at the store_error(error) method that is called when an error is found, it seems it's creating a new record in the Database to log the error. You should be able to check if a user has any error logged via the user.check_errors relationship.
You could do it like this, for example:
class SomeController < ActionController::Base
# ...
before_action :redirect_if_check_errors
# ...
def redirect_if_check_errors
# Assuming you're using Devise or something similar
if current_user && current_user.check_errors.exists?
redirect_to some_error_page_you_create_for_this_path
end
end
end
This will check for these errors in every action of SomeController and redirect the user to an error page you should create, where you render the errors in the user.check_errors relationship.
There are multiple ways to do this, but I still think sending an email from the Job is a better option if you want to actively notify the user. Or perhaps add an interface element that warns the user whenever user.check_errors has stuff there, for example.
I propose that you do this synchronously so that the response can happen directly in the request/response cycle. Perhaps something like this:
# controller
def search
# do your searching
# ...
if search_results.blank?
# call model method, but do it synchrously
purchase_check = PurchaseChecker.call
end
if purchase_check.is_a?(MyError) # Check if it's your error
redirect_to(some_path, flash: { warning: "Warn them"})
end
end
# model, say PurchaseChecker
def call
# do your code
rescue MyError => e
store_error(e)
e # return the error so that the controller can do something with it
end

Catch Error from external API in Rails

I am building a simple web app that sends SMS messages to cell phones using Twilio. I want to ensure that the user has entered a full 10 digit phone number before it will allow a message to attempt to be sent.
When I test it with a less-than or greater-than 10 digit number, in heroku logs, I see Twilio::REST::RequestError (The 'To' number 1234567890 is not a valid phone number.).
I have tried to use a begin/rescue wrapper and am telling it to render text: "Try again with a valid number." and tried a variety of if statements to try to avoid the error.
I am pretty new to Ruby and Rails and Twilio, but I promise i have been through every guide I have found. Any help is greatly appreciated. Full code of my UserController below:
require 'twilio-ruby'
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new(params[:user])
account_sid = '...'
auth_token = '...'
if #user.save
render text: "Wasn't that fun? Hit the back button in your browser to give it another go!"
begin
client = Twilio::REST::Client.new account_sid, auth_token
client.account.sms.messages.create(
from: '+16035093259',
to: #user.phone,
body: #user.message
)
rescue Twilio::REST::RequestError
render text: "Try again with a valid number."
end
else
render :new
end
end
end
I'd extract the SMS sending logic into a separate model/controller and use a background job to process the submitting. The UserController should only handle, well, user creation/modification.
Scaffolding:
$ rails g model sms_job user:references message:text phone submitted_at:datetime
$ rake db:migrate
Model:
class SmsJob < AR::Base
attr_accessible :user_id, :message, :phone
belongs_to :user
validates_presence_of :message, :phone, :user_id
validates :phone,
length: { min: 10 },
format: { with: /\+?\d+/ }
scope :unsubmitted, where(submitted_at: nil)
TWILIO = {
from_no: '...',
account_sid: '...',
auth_token: '...'
}
# find a way to call this method separately from user request
def self.process!
unsubmitted.find_each do |job|
begin
client = Twilio::REST::Client.new TWILIO[:account_sid], TWILIO[:auth_token]
client.account.sms.messages.create(
from: TWILIO[:from_no],
to: job.phone,
body: job.message
)
job.submitted_at = Time.zone.now
job.save
rescue Twilio::REST::RequestError
# maybe set update a tries counter
# or delete job record
# or just ignore this error
end
end
end
end
The controller then should just provide the information that the SMS is going to be send:
# don't forget the 'resources :sms_jobs' in your routes.rb
class SmsJobsController < ApplicationController
# index, update, destroy only for only admin?
def new
#sms_job = SmsJobs.new
end
def create
#sms_job = current_user.sms_jobs.build params[:sms_job]
if #sms_job.save
redirect_to root_url, notice: "Your message is being send!"
else
render :new
end
end
end
For the background processing, have a look at these excellent Railscasts :-) You probably need to workaround some concurrency problems if you have to process many messages and/or Twilio has a long response time (didn't use that service yet).

save_with_captcha is nil in controller

I have home controller and trying to update the field of a table of different controller.
Home controller
class HomeController < ApplicationController
before_action :user_params, only: [:index]
def index
#email = Email.new(user_params)
end
def contact
end
def faq
end
def team
end
def privacy
end
def esave
if !user_params.nil?
if #email.save_with_captcha
flash[:notice] = "Thank you for registering you email address"
redirect_to :action => 'index'
else
render 'esave'
end
end
end
def user_params
params.require(:email).permit(:email, :captcha, :captcha_key) if params[:email]
end
end
So I am trying to create new field of Email Model in my home controller but when I click save it throws this error...
App 24078 stderr: Completed 500 Internal Server Error in 3ms
App 24078 stderr:
App 24078 stderr: NoMethodError (undefined method `save_with_captcha' for nil:NilClass):
App 24078 stderr: app/controllers/home_controller.rb:37:in `esave'
I have email model with defined validations which is like this
class Email < ActiveRecord::Base
apply_simple_captcha :message => "The secret Image and code were different", :add_to_base => true
validates_format_of :email, :with => /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/ , :message => "Invalid Format"
end
So I do not understnd why this save_simple_captcha is nil, any suggestions
Here my 2 cents.
Remember HTTP is stateless. You have defined #email in the index, you have gotten to the esave after page has been refreshed so #email is no longer available.
( Controller is a bit different that straight Ruby Class that get instantiated and variables can be tossed around )
You HAVE TO instantiate the #email for the method your are in.
I don't know what is in your Model so I can just guess the rest.
This #email = Email.find(params[:id]) might or might not work as I don't know your model. ( as mentioned in other comments )
Put your model here too. We might be able to help you out.
PS: It is not a good idea to accept to code on a framework when you don't know a very basic of it :)

Resources