Skip to content

Instantly share code, notes, and snippets.

@tmcdb
Last active August 29, 2015 14:08
Show Gist options
  • Save tmcdb/066f08e2622e0c06b23a to your computer and use it in GitHub Desktop.
Save tmcdb/066f08e2622e0c06b23a to your computer and use it in GitHub Desktop.

Refactoring A Basic Stripe Payment System in Rails

This is part 2 of a Stripe / Rails Tutorial. Go here for Part 1

####Tidying up controllers and views.

Currently the code responsible for creating a charge lives in the orders controller but it's somewhat neater and more conventional to let the order model handle this.

For both of these steps let's refactor our code by moving the Charge making part into a save_and_charge method in Order model, which we'll do in the next section.

Let's cut that Charge creation code out of the controller. We can place it inside a method defined in the Order model:

# app/controllers/orders_controller.rb

def create

    @product = Product.find(params[:product_id])
    @order = @product.orders.new(order_params)

    if @order.save_and_charge

      flash[:success] = "Thanks for purchasing #{@product.name}"
      redirect_to product_order_path(@product, @order)

    else

      flash[:error] = "Oops, something went wrong"
      render :new
  end
end

Copying the Charge code over verbatim will break the app – we won't be able to take payments for products until we've refactored it to look like this:

# app/models/order.rb

def save_and_charge
  Stripe.api_key = Rails.application.secrets.stripe_secret_key
  Stripe::Charge.create(
    :amount => price_in_pence,
    :currency => "gbp",
    :card => stripe_token # obtained with Stripe.js
  )
end

In the code above the card argument is represented by the stripe token generated by our client-side javascript code. We can access it directly with stripe_token because we're passing it in as a whitelisted attribute in our controller's create action thus: @order = @product.orders.new(order_params).

Try making another purchase and confirm that it still works as expected.


Let's build in some error handling by taking the example code from Stripe's Docs.

https://stripe.com/docs/api#errors

Let's look at the code I've just added.

Since we're no longer calling .save in the controller but .save_and_charge we have to make sure the record is actually saved to the database with self.save.

A Stripe::CardError exception would be raised if we pass in 4000 0000 0000 0002 as a card number. Instead of allowing the exception to be raised we're now going to rescue it and do something useful – display it to the user, and print it to the log to help us when we're debugging.

Note that errors generated by the front end validation code will render thanks to this jQuery code that we included earlier.

  $form.find('.payment-errors').text(response.error.message);

It injects the error message returned by the createToken javascript function when invalid card details are submitted. However, when we test for card declines using 4000 0000 0000 0002, the card details are valid so a token is returned instead of an error. The token, then, is passed to the save_and_charge method which tries to raise a Stripe::CardError exception, which we are able to rescue.

# app/models/order.rb

def save_and_charge
  Stripe.api_key = Rails.application.secrets.stripe_secret_key
  Stripe::Charge.create(
    :amount => price_in_pence,
    :currency => "gbp",
    :card => stripe_token # obtained with Stripe.js
  )
rescue Stripe::CardError => e
  body = e.json_body
  err  = body[:error]

  logger.debug = "Error message: #{ err[:message] }"
  errors.add :base, err[:message]

  false
end

###Rescue

By rescuing the exception the rest of the controller code continues to execute and we can tell the user what went wrong.

The code between the first and second rescue will only execute if the app tries to raise the named exception Stripe::CardError. The code between the second rescue and the first end will excute if the app tries to raise any other kind of exception.

Here's what happens if we see a card get declined, or any other type of card error. First we grab the response body from Stripe with body = e.json_body. This gives us a hash which looks like this:

{
  :error => {
    :message => "Your card was declined.",
    :type => "card_error",
    :code => "card_declined",
    :charge => "ch_14qNfD2uVgGd0r3k9oPi1Bvh"
  }
}

We access the error portion of the hash with err = body[:error] and subsequently can access the message with err[:message]. We can use logger.debug to print messages to the log file, all we have to do is pass in a string as an argument. We can interpolate the error message with a string like so: logger.debugg "Message is: #{err[:message]}".

Log files are located in the log folder and are named by their environment. When you're running the server locally logs will be printed to log/development.log.

To display the error message to the user we need to do two things; add the message to the errors on the Order instance, and print the error messages in the view.

In the model code we're adding the error message from Stripe to the errors on the Order instance with errors.add :base, err[:message].

At the top of new order view we can print the error messages like so:

# app/views/orders/new.html.erb
<% @order.errors.full_messages.each do |message| %>

  <p><%= message %></p>
<% end %>

Finally, back in the Order model, if we get a card error from stripe we return false to the controller. This ensures that the falsy code in the if statement of the create action takes control:

#app/controllers/orders_controller.rb
if @order.save_and_charge
  flash[:success] = "Thanks for purchasing #{@product.name}"  # This excecutes if we
  redirect_to product_order_path(@product, @order)            # return something truthy.
else
  flash[:error] = "Oops, something went wrong"                # This excecutes if we
  render :new                                                 # return something falsy.
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment