Francis Ouellet
Francis Ouellet

Reputation: 505

Where and how to handle Stripe exceptions?

I'm building a small proof of concept with Stripe and Ruby on Rails 3.2. So far I've watched the Railscast on how to implement Stripe in a RoR app and it's working really well.

I've built my app by following RailsCast #288 Billing with Stripe. Now my users can add and edit their credit cards and even register to classes and have their credit card billed upon completion.

Now I've been testing with Stripe's numerous test credit cards and I want to catch as many exceptions when raised. I'm using Stripe's example errors in my Registration model as show here:

class Registration < ActiveRecord::Base

  belongs_to :user
  belongs_to :session

  attr_accessible :session_id, :user_id, :session, :user, :stripe_payment_id
  validates :user_id, :uniqueness => {:scope => :session_id}

  def save_with_payment(user, stripe_card_token)
    if valid?
      if user.stripe_customer_id.present?
        charge = Stripe::Charge.create(
            :customer => user.stripe_customer_id,
            :amount => self.session.price.to_i * 100,
            :description => "Registration for #{self.session.name} (Id:#{self.session.id})",
            :currency => 'cad'
        )
      else
        customer = Stripe::Customer.create(
            :email => user.email,
            :card => stripe_card_token,
            :description => user.name
        )
        charge = Stripe::Charge.create(
            :customer => customer.id,
            :amount => self.session.price.to_i * 100,
            :description => "Registration for #{self.session.name} (Id:#{self.session.id})",
            :currency => 'cad'
        )
        user.update_attribute(:stripe_customer_id, customer.id)
      end
      self.stripe_payment_id = charge.id
      save!
    end
  rescue Stripe::CardError => e
    body = e.json_body
    err  = body[:error]
    logger.debug "Status is: #{e.http_status}"
    logger.debug "Type is: #{err[:type]}"
    logger.debug "Code is: #{err[:code]}"
    logger.debug "Param is: #{err[:param]}"
    logger.debug "Message is: #{err[:message]}"
  rescue Stripe::InvalidRequestError => e
    # Invalid parameters were supplied to Stripe's API
  rescue Stripe::AuthenticationError => e
    # Authentication with Stripe's API failed
    # (maybe you changed API keys recently)
  rescue Stripe::APIConnectionError => e
    # Network communication with Stripe failed
  rescue Stripe::StripeError => e
    # Display a very generic error to the user, and maybe send
    # yourself an email
  rescue => e
    # Something else happened, completely unrelated to Stripe
  end
end

I'm merely rescuing from errors right now and not really taking action after one being raised and ultimately I would like to stop the current class registration from happening and redirect a user with a flash error.

I've read about rescure_from but I'm not sure what is the best way to handle of all the possible Stripe errors. I know can't redirect from the model, how would you experts handle this?

Here's my Registration controller:

class Classroom::RegistrationsController < ApplicationController
  before_filter :authenticate_user!

  def new
    if params[:session_id]
      @session = Session.find(params[:session_id])
      @registration = Registration.new(user: current_user, session: @session)
    else
      flash[:error] = "Course session is required"
    end

    rescue ActiveRecord::RecordNotFound
      render file: 'public/404', status: :not_found

  end

  def create
    if params[:session_id]
      @session = Session.find(params[:session_id])
      @registration = Registration.new(user: current_user, session: @session)
      if @registration.save_with_payment(current_user, params[:stripe_card_token])
        flash[:notice] = "Course registration saved with success."
        logger.debug "Course registration saved with success."
        mixpanel.track 'Registered to a session', { :distinct_id => current_user.id,
                                           :id => @session.id,
                                           'Name' => @session.name,
                                           'Description' => @session.description,
                                           'Course' => @session.course.name
        }
        mixpanel.increment current_user.id, { :'Sessions Registered' => 1}
        mixpanel.track_charge(current_user.id, @session.price.to_i)
      else
        flash[:error] = "There was a problem saving the registration."
        logger.debug "There was a problem saving the registration."
      end
      redirect_to root_path
    else
      flash[:error] = "Session required."
      redirect_to root_path
    end
  end

end

Thanks for taking the time to respond, much appreciated!

Francis

Upvotes: 10

Views: 7200

Answers (1)

rovermicrover
rovermicrover

Reputation: 1453

Have you thought of putting the actually Stripe call in a custom validator?

http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validate

That way you could add errors to the object with something like the following

The logic behind this is you only want to save successful transactions as 'transaction' anyway so why not just put the Stripe charge in the validator.

validate :card_validation

def card_validation

    begin
        charge = Stripe::Charge.create(
           :customer => user.stripe_customer_id,
           :amount => self.session.price.to_i * 100,
           :description => "Registration for #{self.session.name} (Id:#{self.session.id})",
           :currency => 'cad'
        )
        etc etc
    rescue => e
      errors.add(:credit_card, e.message)
      #Then you might have a model to log the transaction error.
      Error.create(charge, customer)
    end

end

This way you can handle the errors like any other errors you would get from a entry not saving, instead of giving a blank error message, or having to handle every last error from Stripe.

class Classroom::RegistrationsController < ApplicationController
  before_filter :authenticate_user!

  def create
    if params[:session_id]
      @session = Session.find(params[:session_id])

      params[:registration][:user] = current_user
      params[:registration][:session] = @session
      params[:registration][:stripe_card_token] = params[:stripe_card_token]

      @registration = Registration.new(params[:registration])
      respond_with(@registration) do |format|
        if @registration.save
          format.html {redirect_to root_path, :notice => "SOMETHING HERE TO TELL THEM SUC"}
        else
          format.html {render}
        end
      end
    else
      respond_with do |format|
        format.html {redirect_to root_path, :error => "SOMETHING HERE TO TELL THEM GET SESSION"}
      end
    end
  end

end

Upvotes: 9

Related Questions