Paypal IPN in Rails with Active Merchant

active_merchant | paypal | rails September 16 2006

Active Merchant makes it extremely simple to use Paypal IPN. Here is a simple guide for getting IPN up and running.

Sign up for a Paypal sandbox account

Paypal provides a sandbox environment that mimics their production environment, with the exception that it doesn’t actually process the transactions. This is extremely useful for development and testing. It allows you to create multiple fake accounts and generate bank accounts and credit cards. More information can be found on Paypal’s Testing Instant Payment Notification page.

Unfortunately, I’ve signed up for two different developer accounts and I’ve had trouble logging in with both of them. I’ve tried resetting my password, but I still can’t log in. Fortunately, I already have my sandbox accounts set up and don’t really have a need for it (except to write this guide).

Create a Personal account and add a credit card

After you sign up for your developer account, create a personal sandbox account and add a credit card.

Create a Business account and add a checking

Next, create a business sandbox account and add a checking account.

Install the money gem

sudo gem install money

Install the Active Merchant plugin

script/plugin install http://activemerchant.googlecode.com/svn/trunk/active_merchant

Create a form that submits to Paypal

Include ActiveMerchant::Billing::Integrations in a controller to add Active Merchant’s helpers.

class PaymentsController < ApplicationController
  include ActiveMerchant::Billing::Integrations

  def create
    @enrollment = current_user.enrollments.find(params[:id])
  end
end

In the view, use payment_service_for to create a form that submits to Paypal to process the payment.

<% payment_service_for @enrollment.id, PAYPAL_ACCOUNT,
        :amount => @enrollment.course.deposit, :currency => 'USD',
        :service => :paypal do |service| 

    service.customer :first_name => @enrollment.student.first_name,
        :last_name => @enrollment.student.last_name,
        :phone => @enrollment.student.phone,
        :email => @enrollment.student.email
    service.billing_address :city => @enrollment.student.city,
        :address1 => @enrollment.student.street,
        :state => @enrollment.student.state,
        :country => 'USA',
        :zip => @enrollment.student.zip
    service.item_name "#{@enrollment.course.program} Deposit"
    service.invoice @enrollment.invoice.id
    service.tax '0.00'

    service.notify_url url_for(:only_path => false, :action => 'notify')
    service.return_url url_for(:only_path => false,
        :controller => 'account', :action => 'show')
    service.cancel_return_url url_for(:only_path => false,
        :controller => 'account', :action => 'show') %>

    <!-- display payment summary here -->

    <%= submit_tag 'Make Payment' %>
<% end %>

The code above refers to the constant PAYPAL_ACCOUNT, which I define in environment.rb. I also set Active Merchant to use test mode, which directs it to use Paypal’s sandbox:

unless RAILS_ENV == 'production'
  PAYPAL_ACCOUNT = 'sandboxaccount@example.com'
  ActiveMerchant::Billing::Base.mode = :test
else
  PAYPAL_ACCOUNT = 'paypalaccount@example.com'
end

Create an action that processes the IPN

After the above form submits to Paypal and the user makes a payment, Paypal will post data about the transaction to your server. Set up an action to receive the post:

  def notify
    notify = Paypal::Notification.new(request.raw_post)
    enrollment = Enrollment.find(notify.item_id)

    if notify.acknowledge
      @payment = Payment.find_by_confirmation(notify.transaction_id) ||
        enrollment.invoice.payments.create(:amount => notify.amount,
          :payment_method => 'paypal', :confirmation => notify.transaction_id,
          :description => notify.params['item_name'], :status => notify.status,
          :test => notify.test?)
      begin
        if notify.complete?
          @payment.status = notify.status
        else
          logger.error("Failed to verify Paypal's notification, please investigate")
        end
      rescue => e
        @payment.status = 'Error'
        raise
      ensure
        @payment.save
      end
    end
    render :nothing => true
  end

Depending on the model for your application, this action will obviously look different. The important part is that you pass the raw post data from the request to Paypal::Notification.new, and call notify.acknowledge to connect back to Paypal to verify the data.

Enable IPN

Lastly, log into the business account that you created above, go to “Instant Payment Notification Preferences” in your profile, and set the URL that Paypal should post back to after payments. (Note: this needs to be a publicly accessible URL.)

posted by brandon | updated September 5th 09:10 AM
comments feed

25 comments

  1. Thanks for this, Brandon. You saved me a good bit of time. I just wanted to add that I needed to put “require_gem ‘money’” into my environment.rb file to avoid a NameError exception during the notification phase.

    Haig Didizian Haig Didizian
    December 08, 2006 at 11:01 AM
  2. Wow, that looks great… I’m going to try it for my current project. Thank you

    Frank Frank
    June 12, 2007 at 07:15 AM
  3. I keep getting “uninitialized constant ActiveMerchant (NameError)” when trying to start the server with “ActiveMerchant::Billing::Base.mode = :test” in the environment.rb file.

    I have “require_gem ‘money’” in the environment.rb file.

    Justin Justin
    July 12, 2007 at 05:43 PM
  4. Justin,

    Do you have ActiveMerchant installed as a plugin or a gem? If you have the gem installed, then you also need to do require 'active_merchant' in your environment.rb file

    Brandon Brandon
    July 12, 2007 at 05:49 PM
  5. Solved my problem after a LOT of looking.

    I originally installed ActiveMerchant using “gem install”.

    My problem went away only after I installed ActiveMerchant as a PLUGIN. This is different from a GEM.

    Justin Justin
    July 12, 2007 at 06:19 PM
  6. I tried “require ‘activemerchant’” and got the error:

    warning: already initialized constant OPTIONS

    Justin Justin
    July 12, 2007 at 06:22 PM
  7. note: to get anything out of the notify object you need to put require ‘money’ in the controller.

    inboulder inboulder
    July 15, 2007 at 04:31 PM
  8. If I have require ‘money’ in the environment.rb file I don’t need it in the controller right?

    FYI: I was using “require ‘activemerchant’” in the environment.rb file which wasn’t working. I didn’t realize that I needed an underscore. Adding “require ‘active_merchant’” works just fine. Thanks Brandon.

    Justin Justin
    July 16, 2007 at 10:36 AM
  9. IPN Seems to be working for the most part, but the “If notify.complete?” part is never TRUE. What determines if that is true or not? Is there something I need to do for it to be true? Or is it completely in Paypal’s hands?

    Justin Justin
    July 17, 2007 at 02:49 PM
  10. This is a great example. Thanks for the informative blog post. I’m wondering if maybe you might want to hide a lot of the details in the view in a controller, as in, just post the enrollment and user id and populate and redirect the form. You can certainly encrypt a button through PayPal, but this might give you another way to hide details that someone can’t try and use to create a fake form post with an alteration.

    Observer Observer
    August 09, 2007 at 06:24 PM
  11. Observer,

    The problem is that due to the way Paypal’s website payments work, the user needs to post all that data to paypal. So no matter how you try to obfuscate it, you’re still dependent on the client to post all the details.

    Brandon Brandon
    August 09, 2007 at 10:00 PM
  12. Thanks very much for such a helpful Active Merchant example.

    The command you have listed for installing the AM plugin gives an access denied error, which I believe is because the repository has been moved. The new repository is at: http://activemerchant.googlecode.com/svn/trunk/active_merchant

    Mike Mike
    September 04, 2007 at 02:00 PM
  13. Mike: Thanks, I’ve updated the url in the post.

    Brandon Brandon
    September 05, 2007 at 09:14 AM
  14. Very helpful Brandon, I could able to set up my test paypal account in less than 15 min. Thanks a lot.

    Dinesh Dinesh
    September 08, 2007 at 06:02 PM
  15. Looks good, but, why do I get this on the notify:

    NameError (uninitialized constant AccountController::Paypal):

    Where is this PayPal class? It doesn’t seem to be in active merchant.

    Phil Phil
    September 29, 2007 at 08:15 AM
  16. Thanks for the post, just what I was looking for. Would be nice if PayPal was just set up so easy that we didn’t need a tool like ActiveMerchant though…

    Jordan Brough Jordan Brough
    October 11, 2007 at 01:10 AM
  17. Hi, i have just a little problem. When i process order and i look for the result in my paypal account i find my order, but the big problem is my total is divised by 100. Result i process an order with a total : 60 and the result in my paypal account is 0,60

    Does someone can help me ??

    Matthias Matthias
    October 23, 2007 at 05:35 AM
  18. Matthias,

    The amount field is expecting a Money object, and if it doesn’t get one, it converts the integer, treating it as cents.

    So you’ll want to pass in the number as cents. If the amount is being stored in a model, you might want to check out acts_as_money

    Brandon Brandon
    October 23, 2007 at 05:07 PM
  19. thanks brandon

    that’s the part who content the price

    def populate_order 
      for cart_item in @cart.cart_items 
        order_item = OrderItem.new( 
          :book_id => cart_item.book_id, 
          :price => cart_item.price - current_user.offre, 
          :amount => cart_item.amount,
      :form_choice => cart_item.form_choice,
      :time_choice => cart_item.time_choice,
      :expire_at => cart_item.expire_at    
        ) 
        @order.order_items << order_item 
      end 
    end

    I don’t really understand your solution…

    Matthias Matthias
    October 25, 2007 at 11:14 AM
  20. Phil, If you are still hitting this issue {NameError (uninitialized constant AccountController::Paypal): Where is this PayPal class? It doesn’t seem to be in active merchant.}

    you might want to try ActiveMerchant::Billing::Integrations::Paypal.

    Additionally ensure that you have done a require ‘active_merchant’ in your environment.rb

    Vishwa Vishwa
    December 08, 2007 at 11:04 AM
  21. Hi, this is cool, but I get:

    NameError (undefined local variable or method `‘money’’ for main:Object): /app/controllers/payments_controller.rb:2

    I have installed the money gem, do I need to do a require ‘money’ in the environment.rb file? I tried that and I get:

    /Library/Ruby/Gems/1.8/gems/money-1.7.1/lib/support/cattr_accessor.rb:7:in `cattr_reader’: undefined method `id2name’ for {:instance_writer=>false}:Hash (NoMethodError) from /Library/Ruby/Gems/1.8/gems/money-1.7.1/lib/support/cattr_accessor.rb:5:in `each’ from /Library/Ruby/Gems/1.8/gems/money-1.7.1/lib/support/cattr_accessor.rb:5:in `cattr_reader’ from /Library/Ruby/Gems/1.8/gems/money-1.7.1/lib/support/cattr_accessor.rb:54:in `cattr_accessor’ from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.6/lib/active_record/base.rb:268

    at server startup, any ideas?

    Joel Joel
    February 14, 2008 at 01:53 PM
  22. gem install money, maybe?

    Roger Pack Roger Pack
    March 14, 2008 at 02:31 AM
  23. Just a thought, but when PayPal has to convert currencies, they send back the status as pending, which causes the “If notify.complete?” to return false, so you may want to change it to something like this to perform different actions on different statuses.
    if notify.acknowledge
          begin
            case notify.status
            when "Completed" 
              @order.paypal_status = notify.status
              @order.transaction_id = notify.transaction_id
            when "Pending" 
              @order.paypal_status = "Pending because: #{params[:pending_reason]}" 
              @order.transaction_id = notify.transaction_id
            else
              @order.paypal_status = notify.status
              logger.error("Failed to verify Paypal's notification, please investigate")
            end
          ensure
            @order.save
          end
        end
    
    Noah Noah
    May 03, 2008 at 05:31 PM
  24. This definitely clears things up further! Thank you. However, how is payment.rb supposed to be?

    Ramon Tayag Ramon Tayag
    May 30, 2008 at 06:56 AM
  25. This looks great…last thing I’m wondering is how to process anything other than a successful order. Such as a subscription cancellation, etc. Are these in the notify.status variable?

    Brian Brian
    June 27, 2008 at 12:53 AM

Speak your mind:

(Required)

(Required)


(You may use textile in your comments.)

About

I'm Brandon Keepers, a web application developer that likes beautiful code, valid markup and adherence to standards. As a part of Collective Idea in Holland, Michigan, I practice Agile software development primarily using Ruby on Rails.

-86.103171 42.785037

Contact:

more »

Syndicate