1

In my Rails 4 app I have a Service object that handles communication with Stripe Payments Processor. I want it as a service object so that multiple Controllers/Models can utilize the methods within it.

However, I also need to be able to trap errors when communicating with the Stripe API which then causes the problem as the errors need to be assigned to a particular object.

Here is a method in my StripeCommunicator.rb class:

def create_customer(token,object)
  customer = Stripe::Customer.create(:description => 'Accommodation', :email => object.email, :card => token)
  return customer

rescue Stripe::CardError => e
  @account.errors.add :base, e.message
  false
end

as you can see - the errors are being added to the @account object - which essentially renders it useless when I want to use this method from another controller with a View that refers to another object to display errors.

Any ideas?

1 Answer 1

3

Simplest thing is to just pass the @account instance in as another argument. Errors is going to be on any model instance, e.g.

def create_customer(token,object,model_instance)
  Stripe::Customer.create(description: 'Accommodation', email: object.email, card: token)
  # return customer <- don't need this. whatever is last evaluated will be returned
rescue Stripe::CardError => e
  model_instance.errors.add :base, e.message
  false
end

If you were doing the error handling in the controller instead of a service object, you could take advantage of rescue_from which can handle exceptions falling out from action methods, e.g. in your controller or ApplicationController, etc., do the following:

rescue_from Stripe::CardError, with: :add_error_message_to_base

def add_error_message_to_base(e)
  # this assumes that you set @instance in the controller's action method.
  @instance.errors.add :base, e.message
  respond_with @instance
end

or more generically:

rescue_from Stripe::CardError, with: :add_error_message_to_base

def add_error_message_to_base(e)
  model_class_name = self.class.name.chomp('Controller').split('::').last.singularize
  instance_value = instance_variable_get("@#{model_class_name}")
  instance_value.errors.add :base, e.message if instance_value
  respond_with instance_value
end

or in a concern, you could do either of the above, putting the rescue_from into the included block:

module StripeErrorHandling
  extend ::ActiveSupport::Concern

  included do
    rescue_from Stripe::CardError, with: :add_error_message_to_base
  end

  def add_error_message_to_base(e)
    # see comment above...
    @instance.errors.add :base, e.message
    respond_with @instance
  end
end

And you can use config.exceptions_app to handle errors at the Rack-level as José Valim describes here.

You could also inherit the method vs. having a separate service class, or have a concern/module. You might even do through hooks, e.g.:

# not exactly what you were doing but just for example.
# could put in app/controller/concerns among other places.
module ActionsCreateStripeCustomer
  extend ::ActiveSupport::Concern

  included do
    around_action :create_stripe_customer
  end

  def create_stripe_customer
    # this (indirectly) calls the action method, and you will
    # set @instance in your action method for this example.
    yield
    customer = Stripe::Customer.find_or_create_by(description: 'Accommodation', email: object.email, card: token)
    # could set customer on @instance here and save if needed, etc.
  rescue Stripe::CardError => e
    if @instance
      @instance.errors.add :base, e.message
      respond_with @instance
    else
      logger.warn("Expected @instance to be set by #{self.class.name}##{params[:action]}")
      raise e
    end
  end
end

Then in the controller:

include ActionsCreateStripeCustomer

There is also before_action, after_action, etc. Also, you can just include modules and when instance methods are called they call on the including class instance first, then the first included module, then the second, etc. if you do super if defined?(super) to call the prior method, and it automatically puts in all the arguments and block.

And, if it were about getting the model class name rather than the instance, that is easy, too. Say the class you were calling from was AccountStripeCommunicator, then @model_class after the following would be Account:

qualified_class_name = self.class.name.chomp('StripeCommunictor')
@model_class = qualified_class_name.split('::').last.singularize.constantize

All kinds of possibilities.

Sign up to request clarification or add additional context in comments.

9 Comments

Thanks for this very detailed response. Really appreciate it! I would like to do error handling in the Controller but I wasn't sure how - mind me asking how I would get rescue_from to work from my controller as I think that will be the best option for me?
Updated answer. Hope that helps.
After more experimenting I'm getting some unexpected behaviour. I have removed my rescue statements from the service object and added rescue_from and the add_error_message_to_base(e) method to the calling Controller but when I force an error I just get presented with a blank page as if everything has stopped running. Before, it would re-direct!???
I have added render :new in the add_error_message_to_base(e) and seems to be working now.
y sorry about that. since the error is being added to the instance for the view, maybe a more generic way to do it would be render params[:action]. added that to the example
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.