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.