Integrating Paypal Express with Rails 3.1, Part 1

Ever wanted to add this button to your site?

I spent a few weeks figuring out how to make the Paypal checkout button work properly on my site. There were a lot of resources on the web, but none of them were complete. After a lot of trial and error, here is the step-by-step guide that should provide everything you need.

Adding the Paypal Button to Your Site

The first step is the simplest one: add the button to your site!

Find the page on your site where you want to add the button, and paste the following code to add a clickable Paypal checkout button. Please note that I use Haml for my project.

= link_to image_tag("https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif"), :controller => 'paypal_express', :action => 'checkout'

You will notice that I have a controller called ‘PaypalExpress’ with a custom action called ‘checkout’. We don’t have such controller yet, but at least you have a button on your site now.

Modifying Rails routes.rb

Try using Firebug to inspect the Paypal checkout button. You will notice that the href is ‘paypal_express/checkout’. So, to make the link work properly when we click on it, we need to add the following line to routes.rb:

  get "paypal_express/checkout"

Of course, clicking on it will make Rails complain that such controller doesn’t exist. So now it’s time to create the controller.

Creating PaypalExpressController

Here’s what my PaypalExpressController looks like:

class PaypalExpressController < ApplicationController
  before_filter :assigns_gateway

  include ActiveMerchant::Billing
  include PaypalExpressHelper

  def checkout
    total_as_cents, setup_purchase_params = get_setup_purchase_params @cart, request
    setup_response = @gateway.setup_purchase(total_as_cents, setup_purchase_params)
    redirect_to @gateway.redirect_url_for(setup_response.token)
  end

  private
    def assigns_gateway
      @gateway ||= PaypalExpressGateway.new(
        :login => PaypalLogin.login,
        :password => PaypalLogin.password,
        :signature => PaypalLogin.signature,
      )
    end
end

There are quite a few ivars/classes/modules/methods used in the controller, namely:

  • ActiveMerchant::Billing and PaypalExpressGateway
  • PaypalLogin
  • PaypalExpressHelper and get_setup_purchase_params method
  • @cart

A lot of code right? No worries. I will go over the details you need to know. Before we dive into the details of what the controller does, let’s take care of all the dependencies I just mentioned.

ActiveMerchant Gem

The first part of the controller I want you to pay attention to is ActiveMerchant::Billing and PaypalExpressGateway class. These two come from ActiveMerchant. It provides a nice wrapper around Paypal APIs so that you don’t have to go through this kind of setup.

To install the gem, just put this into your Gemfile.

gem 'activemerchant'

Once you have it installed, please don’t forget to configure ActiveMerchant so that it will be in test mode for your development environment.

YourApp::Application.configure do

  # Some other stuff in development.rb
  
  # Force ActiveMerchant into test mode
  config.after_initialize do
    ActiveMerchant::Billing::Base.mode = :test
  end
end

Optionally, you could put the same code in your config/environments/test.rb.

Authenticating with Paypal is Required in All Stages
The next part of the controller is the PaypalLogin class. PaypalLogin is a class I have defined in my project to provide the Paypal credentials which are stored in my YAML file. To achieve this, I used the SettingsLogic gem. Please go through the page on Github to see how it’s used.

As you can see, Paypal requires the following credentials for it to properly work:

  • login
  • password
  • signature

Here’s an example of Paypal credentials taken from Railscasts.com:

{
  :login => "seller_1229899173_biz_api1.railscasts.com",
  :password => "FXWU58S7KXFC6HBE",
  :signature => "AGjv6SW.mTiKxtkm6L9DcSUCUgePAUDQ3L-kTdszkPG8mRfjaRZDYtSu"
}

To obtain these credentials, for development and testing, sign up for a sandbox account here. I found following the direction given by Ryan Bates’s Railscasts to be really helpful, though it’s a little outdated.

For production, you will need to sign up for Paypal Express Checkout, Website Payment Standard or Website Payment Pro. Discussing the differences between each one is beyond the scope of this guide and should be researched to use the best one for your application.

PaypalExpressHelper

PaypalExpressHelper is a helper class I created on my own. Here’s what my PaypalExpressHelper looks like:

module PaypalExpressHelper
  def get_setup_purchase_params(cart, request)
    subtotal, shipping, total = get_totals(cart)
    return to_cents(total), {
      :ip => request.remote_ip,
      :return_url => url_for(:action => 'review', :only_path => false),
      :cancel_return_url => home_url,
      :subtotal => to_cents(subtotal),
      :shipping => to_cents(shipping),
      :handling => 0,
      :tax =>      0,
      :allow_note =>  true,
      :items => get_items(cart),
    }
  end

  def get_shipping(cart)
    # define your own shipping rule based on your cart here
    # this method should return an integer
  end

  def get_items(cart)
    cart.line_items.collect do |line_item|
      product = line_item.product

      {
        :name => product.title, 
        :number => product.serial_number, 
        :quantity => line_item.quantity, 
        :amount => to_cents(product.price), 
      }
    end
  end

  def get_totals(cart)
    subtotal = cart.subtotal
    shipping = get_shipping(cart)
    total = subtotal + shipping
    return subtotal, shipping, total
  end

  def to_cents(money)
    (money*100).round
  end
end

This class provides all the information that ActiveMerchant needs to talk to Paypal.

A few of things I want to point out regarding the code above:

:ip => request.remote_ip,
:return_url => url_for(:action => 'review', :only_path => false),
:cancel_return_url => home_url,

  • ‘review’ is another action of PaypalExpressController. We will cover it in the next blog post.
  • ‘home_url’ is my application’s home page URL. So for your application, you will need to come up with your own.

@cart

@cart is an ActiveRecord model in my application and it :has_many :products and :has_many :line_items. You can study how it’s used in PaypalExpressHelper.

Now we are ready to understand what the controller does.

PaypalExpressController#checkout

So, here’s what happens when the Paypal checkout button is clicked.

  1. PaypalExpressController#checkout action is called because that is how our routes.rb is setup.
  2. But since our PaypalExpressController has a before_filter, PaypalExpressController#assigns_gateway will get called first.
  3. Using credentials provided by PaypalLogin class, it creates a ActiveMerchant::Billing::PaypalExpressGateway object.
  4. Based on Rails request and data stored in the shopping cart, it calls PaypalExpressHelper.get_setup_purchase_params() to get parameters that ActiveMerchant needs.
  5. Calls ActiveMerchant::Billing::PaypalExpressGateway.setup_purchase and passes the parameters. The parameters include:
    • First parameter is an integer: The total amount you plan to charge your customer in cents, NOT dollars.
    • Second parameter is a hash (see PaypalExpressHelper.get_setup_purchase_params) which includes:
      • Your customer’s IP address
      • Return URL: The page (URL) you want your customer to see when your customer returns from Paypal site.
      • Cancel URL: The page (URL) you want your customer to see in case your customer clicks cancel on Paypal site.
      • Subtotal, shipping, handling, tax… all in cents
      • Allow note (set to true) will allow your customer to add notes as part of the transaction. It’s a small, bearly visible <textarea> on the Paypal checkout page
      • List of items that your customers wanted to purchase.  See PaypalExpressHelper.get_items method to see what the array of hashes looks like.
  6. Finally, based on the response from Paypal, redirect to Paypal website.
Coming up Next…
The remaining PaypalExpressController actions will be covered in the next post. Stay tuned!
Conversation
  • Dave Harrison says:

    thanks for this, when’s the next one available?

    • Sivabudh Umpudh says:

      Hi Dave,

      Thanks for your comments. The next one should be up in a couple of weeks. =)

  • Excelent, good work.

  • […] Picking up from my last post, we will now look at the next PaypalExpressController action: review. Here’s the […]

  • martin says:

    what if you add taxes?
    i am getting paypal error “The totals of the cart item amounts do not match order amounts.” because i have to substract the tax amount from every item’s amount which leads to rounding issues because of the overall total ending up one cent above or below the tax-less total…

    any suggestions, anyone?

  • victor says:

    I was playing with paypal express gem the last month, and didnt works for me, but this tutorial helps me a lot, I think will be very helpfull. Thanks, and waiting for Part 2.

  • richard lewis says:

    hi, i have followed tutorial, thanks for making it clear, but as a newbie to rails ive made a mistake somewhere and was wondering if you could help me.. i think ive covered everything you have stated bit i get this error after i click the paypal link

    NameError in PaypalExpressController#checkout
    uninitialized constant PaypalExpressController::PaypalLogin
    I am unsure to what this means

    • Derko says:

      Hey did you ever get a fix for this?

    • chester says:

      Make sure you have a paypal_express model:

      class PaypalLogin < Settingslogic
      source "#{Rails.root}/config/paypal_login.yml"
      namespace Rails.env
      end

  • Attractive component to content. I just stumbled upon your site and in accession capital to assert that I acquire in fact loved account your blog posts. Any way I’ll be subscribing for your feeds and even I achievement you access constantly quickly.

  • Serguei says:

    What is not covered is @cart variable in PaypalExpressController#checkout method

  • Comments are closed.