opensoul.org

Paypal IPN in Rails with Active Merchant

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.)

active_merchant, paypal, popular, and rails September 16, 2006

45 Comments

  1. Haig Didizian Haig Didizian December 8, 2006

    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.

  2. Frank Frank June 12, 2007

    Wow, that looks great… I’m going to try it for my current project. Thank you

  3. Justin Justin July 12, 2007

    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.

  4. Brandon Brandon July 12, 2007

    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

  5. Justin Justin July 12, 2007

    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.

  6. Justin Justin July 12, 2007

    I tried “require ‘activemerchant’” and got the error:

    warning: already initialized constant OPTIONS

  7. inboulder inboulder July 15, 2007

    note: to get anything out of the notify object you need to put require ‘money’ in the controller.

  8. Justin Justin July 16, 2007

    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.

  9. Justin Justin July 17, 2007

    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?

  10. Observer Observer August 9, 2007

    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.

  11. Brandon Brandon August 9, 2007

    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.

  12. Mike Mike September 4, 2007

    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

  13. Brandon Brandon September 5, 2007

    Mike: Thanks, I’ve updated the url in the post.

  14. Dinesh Dinesh September 8, 2007

    Very helpful Brandon, I could able to set up my test paypal account in less than 15 min. Thanks a lot.

  15. Phil Phil September 29, 2007

    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.

  16. Jordan Brough Jordan Brough October 11, 2007

    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…

  17. Matthias Matthias October 23, 2007

    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 ??

  18. Brandon Brandon October 23, 2007

    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

  19. Matthias Matthias October 25, 2007

    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…

  20. Vishwa Vishwa December 8, 2007

    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

  21. Joel Joel February 14, 2008

    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?

  22. Roger Pack Roger Pack March 14, 2008

    gem install money, maybe?

  23. Noah Noah May 3, 2008

    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

  24. Ramon Tayag Ramon Tayag May 30, 2008

    This definitely clears things up further! Thank you. However, how is payment.rb supposed to be?

  25. Brian Brian June 27, 2008

    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?

  26. Spencer Alexander Spencer Alexander August 5, 2008

    Great article! I’ve had a tough time getting everything set up with paypal and active merchant (not much documentation), and it’s great to come across well written tutorials like this.

    Keep up the good work!

  27. Spencer Alexander Spencer Alexander August 5, 2008

    Question regarding security: On the notify action, where we format the POST we get from paypal using Paypal::Notification.new(request.raw_post), do we need to worry at all about security and the validity of the POST? In otherwords, does Paypal::Notification automatically communicate with Paypal one final time to ensure that the POST is legit?

    Thanks again for the great tutorial!

  28. Dave Clausen Dave Clausen September 2, 2008

    Spencer, I’m dealing with the security issue by using a shared secret in the POST url as outlined on page 26-27 of Paypal’s Order Management Integration Guide located at: https://www.paypal.com/en_US/pdf/PP_OrderManagement_IntegrationGuide.pdf

    Hope that helps.

  29. skylarking skylarking November 14, 2008

    Thanks for your great article! You did a great job in explaining how to integrate the wonderful AM plugin to in an existing rails app. Now, everything seems to work flawlessly here, except for 1 thing. Actually I’ve setup my paymentscontroller actions to be: success, cancel and ipn. It seems I can succesfully pay by using the sandbox paypal account or cancel by going back to my corresponding success or cancel pages (actions), but I am having an hard time trying to understand why each time I receive the paypal post request back to my app it just “hangs” there ( actually I got the IPN since i can see in console all the params paypal is sending). It seems I cannot actually create my @payment. I’ve understand that by default the payment status is set on “Pending” and it changes to “Complete” when you accept it by accessing your SELLER/BUSINESS paypal account. Now, since the action(the ipn as well as the success and cancel) is setup into my form helper and i haven’t setup an IPN URL in paypal, would it be possible to be notified when the status of the payment changes from pending to complete?Or do i need to actually configure the ipn url to be a public one by creating a new controller and action and set up that one url in paypal?Also, does the Notification.acknowledge method check only for a correct post back to paypal when it first posts the IPN to my site or it does check that the status says ‘Complete’, reason why i cannot actually save my @payment to the database seems in the IPN it says ‘Pending’? Last but not least since I am using named routes for my success, cancel and ipn actions, which methods should they have?Except for the redirect from paypal (success and cancel that should be :get) would the ipn be a :post? Thanks in advance

  30. skylarking skylarking November 14, 2008

    Well, ok after a lot of headaches I’ve found out (thanks to this post on http://groups.google.com/group/activemerchant/browse_thread/thread/3198117936621ff7 by Cody Fauser ) that:

    1) DO NOT KNOW WHY??? – DO NOT ASK ME since i wasted 3 days after this.. but it seems that following your guidelines on the site my gateway (PAYPAL STANDARD) doesn’t use notifications and just redirects the customer’s browser back to the seller’s site ( my success action in paymentscontroller).

    Solution to this, do not ask me why? is to get rid of the notify url aka ipn action in the form helper and just move the whole notification thing into my success action of course doing there all the logic. This way it works fine, I’d like to receive any feedback from someone with more experience working with rails paypal and Am in general

  31. arash arash November 18, 2008

    Possible security hole? The confirmation never checks to make sure the receivers email address in the Paypal IPN is equal to the PAYPAL_ACCOUNT variable. Does this need to be done?

    Eg. If a hacker takes the form variables that are generated by your app, and uses it for a payment on his own Website Payments Pro Merchant Paypal account and configures it to send its IPN notification to your controller, does that not effectively trick your app into thinking that a payment was sent (since the notify.acknowledge will be true since it is a real IPN notification sent by Paypal). Or does notify.acknowledge check to make sure the receiver_email in the IPN equals PAYPAL_ACCOUNT.

    Please let me know as I am pondering this.

  32. Todd Todd December 16, 2008

    Thanks for the write-up. I’m using AM to process my IPNs but I’m having some problems. PayPal is continuing to send IPNs after I call notify.acknowledge, so I’m assuming there is something wrong with the acknowledgment. I posted something on PayPal and this was the response I got

    http://www.paypaldeveloper.com/pdn/board/message?board.id=ipn&thread.id=15228

    Has anyone else run into this issue? Any ideas? I can process the IPN without any problems, but I don’t want PayPal re-sending IPNs when its not necessary.

    Thanks.

  33. Brandon Brandon December 23, 2008

    Todd,

    Keep in mind that Paypal will send you an IPN any time the status of the payment changes. For example, for an e-check, they’ll send the IPN with a pending status when the check is submitted, and then again when the check clears. So check notify.status to see if that is changing.

  34. JJ JJ January 27, 2009

    Hi,

    I have errors in notify action
    if notify.acknowledge campaign = Campaign.find(notify.item_id) begin

    if notify.complete? and campaign.current_cost == notify.amount

    .
    .
    .


    campaign.current_cost is float and the error is “undefined method `cents’ for 240.0:Float”


    Another error


    if notify.acknowledge campaign = Campaign.find(notify.item_id) begin</p> if notify.complete? campaign.user.account.update_attribute(:balance, campaign.user.account.balance + notify.amount) <p>.<br /> .<br /> .<br />


    Balance is float and the error is "Money can’t be coerced into Float "


    Any ideas?

  35. Brandon Brandon January 27, 2009

    JJ,

    Your model attributes need to be Money objects. See http://github.com/collectiveidea/money

  36. JJ JJ January 28, 2009

    I have another error:

    undefined method `money’ for #

    [code]
    class Campaign < ActiveRecord::Base
    money :current_cost
    end
    [/code]

    in environment.rb I have

    require ‘money’

  37. Brandon Brandon January 28, 2009

    JJ: Make sure you restart your server after installing plugins.

  38. JJ JJ January 29, 2009

    With installed plugin collectiveidea-money and

    money :current_cost #campaign.rb:6

    I can’t restart server:

    C:/ruby/ruby/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/core_ext/hash/keys.rb:47:in `assert_valid_keys’: Unknown key(s): constructor, converter (ArgumentError)
    from C:/ruby/ruby/lib/ruby/gems/1.8/gems/activerecord-2.1.2/lib/active_record/aggregations.rb:145:in `composed_of’
    from C:/ruby/rails_apps/project_1/vendor/plugins/collectiveidea-money/lib/money/rails.rb:16:in `money’
    from C:/ruby/rails_apps/project_1/app/models/campaign.rb:6
    from C:/ruby/ruby/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:216:in `load_without_new_constant_marking’
    from C:/ruby/ruby/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:216:in `load_file’
    from C:/ruby/ruby/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:355:in `new_constants_in’
    from C:/ruby/ruby/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:215:in `load_file’
    from C:/ruby/ruby/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:96:in `require_or_load’
    … 36 levels…
    from C:/ruby/ruby/lib/ruby/gems/1.8/gems/rails-2.1.2/lib/commands/server.rb:39
    from C:/ruby/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require’
    from C:/ruby/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require’
    from script/server:3

    without this plugin (with money :current_cost) I have error:

    undefined method `money’ for #

  39. Don Don July 13, 2009

    How to add another parametr to FORM (:lc => “EN” – language) ?

  40. JJ JJ October 19, 2009

    to add another parametr to FORM (:lc => “EN” – language) ?[/quote]

    Dirty hack:


    function AddLangToPaypalForm(lang){


    var el = document.createElement(‘input’);
    el.setAttribute(‘type’, ‘hidden’);
    el.setAttribute(‘name’, ‘lc’);
    el.setAttribute(‘value’, lang);
    var form = document.getElementById(‘paypal_form’);
    form.appendChild(el);


    }

  41. JJ JJ November 2, 2009

    How to check my paypal balance via Active Merchant?

  42. KeenToLearnRails KeenToLearnRails March 10, 2010

    Hi,

    Would anyone help me to resolve this problem “email address for the business is not present in the encrypted blob”? I have already spent two days trying to resolve this problme with no success.

    I am building an on-line shopping application using Ruby on Rails. The payment is made via paypal. It works only I didn’t encrypted the payment information. The problem is it always returns an error stating that “email address for the business is not present in the encrypted blob”.. I follow the RailsCast 143 to implement the paypal security. The following are the step how to i implment the paypal security.

    1. Log on the linux box and create a private key and my public certificate
    private key
    openssl genrsa -out app_key.pem 1024

    My Public Certificate
    openssl req -new -key app_key.pem -x509 -days 365 -out app_cert.pem

    2. Upload my public certificate to Paypal sandbox and get the CERT_ID

    3. Download Paypal public certificate: paypal_cert.pem

    Now I have 3 files under the certs directory
    • app_cert.pem
    • app_key.pem
    • paypal_cert.pem

    In my controller, it calls the encrypt function

    values = {
    :business => “help.s_1260152087_biz@gmail.com”,
    :cmd => “cart”,
    :upload => 1,
    :currency_code => “AUD”,
    :handling_cart => @order_log.postage,
    :return => return_paypal_url,
    :notify_url => notify_return_url,
    :cert_id => “STVF85MBT8XWS”
    }
    counter = 0
    @orders = Order.find(:all,:conditions => { :order_log_id => order_log_id.to_i })
    @orders.each do | order |
    counter = counter + 1
    RAILS_DEFAULT_LOGGER.info “item name[” + order.item_name + " |price[" + order.price.to_s + “]”
    values.merge!({
    “item_number_#{counter}” => counter,
    “item_name_#{counter}” => order.item_name,
    “amount_#{counter}” => order.price,
    “quantity_#{counter}” => order.quantity
    })
    end
    encrypted_string = encrypt_for_paypal(values.to
    query)

    ===
    class ShippingDetail < ActiveRecord::Base
    PAYPAL_CERT_PEM = File.read(“#{Rails.root}/certs/paypal_cert.pem”)
    APP_CERT_PEM = File.read(“#{Rails.root}/certs/app_cert.pem”)
    APP_KEY_PEM = File.read(“#{Rails.root}/certs/app_key.pem”)

    def encrypt_for_paypal(values)
    signed = OpenSSL::PKCS7::sign(OpenSSL::X509::Certificate.new(APP_CERT_PEM), OpenSSL::PKey::RSA.new(APP_KEY_PEM, ’’), values.map { |k, v| “#{k}=#{v}” }.join(“\n”), [], OpenSSL::PKCS7::BINARY)
    OpenSSL::PKCS7::encrypt([OpenSSL::X509::Certificate.new(PAYPAL_CERT_PEM)], signed.to_der, OpenSSL::Cipher::Cipher::new(“DES3”), OpenSSL::PKCS7::BINARY).to_s.gsub(“\n”, "")
    end

    end
    ===

    Web Page = The value of the encrypted field is encrypted.

    ==
    But the Paypal returns error “Email address for the business is not presented in the encrypted blob”

    Please help me to resolve this proble.. Because of this problem, we cannot launch our web site.

  43. Michael Michael April 2, 2010

    Hi Brandon,

    I’m wondering whether this code is still usable with Rails 2.3.5 and Ruby 1.8.7? It’s been 4 years since you originally posted this code making me wonder if it’s obsolete.

    If not, then you are the only person that I could find who has given me all the information I need to integrate Paypal.

    Thanks !

  44. Brandon Brandon April 3, 2010

    Michael:

    I haven’t used this for a while, but I’m pretty sure it still works.

  45. Snhxqkxk Snhxqkxk January 10, 2011

    Pornhub 983

My name is Brandon Keepers. I like to build things, usually in Ruby or JavaScript. I work at GitHub and live in Holland, MI.

Popular Posts