PayPal Adaptive with Rails 3

Screen Shot 2011-10-19 at 12.37.56 PM

As usual the PayPal documentation is quite messy and disorganised and thus I thought I’d document how I managed to get PayPal Adaptive working on a marketplace style website.

Warning!

Please use this post as a guide as to how the paypal API connects with your rails app. As Jamesw in the comments below points out, I have not created an adequate way of recording all details of each transaction; something that is no doubt required by law. So perhaps take a look at his comment after reading this. Hopefully you can work out a way to do this :)

Setup

After some searching I found that the best gem to use right now is paypal_adaptive. ActiveMerchant currently does not support PayPal Adaptive (there is a gem that add’s it in but it does not seem to be maintained.)

# Gemfile
gem 'paypal_adaptive'

How it works

PayPal Adaptive is relatively simple, yet the messy documentation can make it appear daunting. Put simply, this is how I did it:

  1. create paypal_adaptive.yml in your config folder and add your details
  2. create a resource to handle payments – I created a “Purchases” resource (model: Purchase)
  3. when the user clicks a buy button it invokes an action I called “buy” in my PurchasesController
  4. in the “buy” action I firstly create a new Purchase item (Purchase.new) and then add in some details like user_id, product_id, amount etc.
  5. I then invoke a model method called “pay” on my newly created Purchase object (purchase.pay)
  6. in my Purchase model I define the “pay” method and inside it I finally make use of the paypal_adaptive gem by creating its Request class (PaypalAdaptive::Request.new)
  7. I add all the necessary data I want to ship off to PayPal into a hash, and then invoke the Request.pay method, passing in that hash of data. This makes an API call to PayPal, who then replies with a confirmation that this request is successful
  8. If the API call is successful I redirect the user off to PayPal to sign in and confirm the payment
  9. Once the user makes the payment, PayPal sends an IPN (instant payment notification) to your specified IPN address – which I routed to a method in my PurchasesController
  10. In this method, I find the the Purchase object and mark it as paid (purchase.paid = true) and then it’s done!

paypal_adaptive.yml

Go here to create a sandbox account (you will need it). Once logged in go to “Create a preconfigured account”. Create two accounts – one buyer and one seller. If you are using chained or parallel payments (payments that are split amongst more than one person) then create some more accounts.

Click on Api Credentials in the left hand side panel.

Now fill out your paypal_adaptive.yml using those credentials (also use the application_id I provide below – this is the testing application_id provided by www.x.com

development:
  environment: "sandbox"
  username: "platts_xxxxxxxx_biz_api1.gmail.com"
  password: "xxxxxxxxxxxx"
  signature: "xxxxxxx"
  application_id: "APP-80W284485P519543T"

test:
  environment: "sandbox"
  username: "platts_xxxxxxxx_biz_api1.gmail.com"
  password: "xxxxxxxx"
  signature: "xxxxxxxx"
  application_id: "APP-80W284485P519543T"

production:
  environment: "production"
  username: "my_production_username"
  password: "my_production_password"
  signature: "my_production_signature"
  application_id: "my_production_app_id"

Create controller action to handle a buy request

Here you only really need the amount of money to be paid and a list of the emails you want that money to go to. So write your logic to work that out and then make a call to PayPal to setup the purchase.

 pay_request = PaypalAdaptive::Request.new
    data = {
      "returnUrl" => return_url,
      "requestEnvelope" => {"errorLanguage" => "en_US"},
      "currencyCode" => "USD",
      "receiverList" =>
              { "receiver" => [
                {"email" => "platts_xxxxxxxx_biz@gmail.com", "amount"=> amount}
              ]},
      "cancelUrl" => cancel_url,
      "actionType" => "PAY",
      "ipnNotificationUrl" => ipn_url
    }

    #To do chained payments, just add a primary boolean flag:{“receiver”=> [{"email"=>"PRIMARY", "amount"=>"100.00", "primary" => true}, {"email"=>"OTHER", "amount"=>"75.00", "primary" => false}]}

    pay_response = pay_request.pay(data)

    if pay_response.success?
        # Send user to paypal
        redirect_to pay_response.approve_paypal_payment_url
    else
        puts pay_response.errors.first['message']
        redirect_to "/", notice: "Something went wrong. Please contact support."
    end

Handling the IPN call

I route my IPN call from PayPal to this method:

   def ipn_notification
    ipn = PaypalAdaptive::IpnNotification.new
    ipn.send_back(request.raw_post)

    if ipn.verified?
      logger.info "IT WORKED"
    else
      logger.info "IT DIDNT WORK"
    end

    render nothing: true
  end

Unfortunately if you are on localhost, PayPal can’t send you the IPN, and hence there is a problem with testing this whole process. Ryan Bates’ solution is to use curl to mimic an IPN request. However as you can see in the code above, we make another request to PayPal confirming that the IPN is real. So even with curl sending a fake IPN, we run into problems. I’m going to go hunt for solutions now, but please comment if you have any ideas.

Tagged: ,

About the Author

Plattsi | Other Articles

A twenty something web developer and entrepreneur from Sydney, Australia. Loves building web applications that are both easy and fun to use (and don't require manuals).

  • Miguel Michelsongs

    i normaly open a port in my router and set the public ip in the ipn route, that way you can test it locally :)

  • Jamesw

    Just a quick note, it is rally bad practice to use foreign keys for your purxhases model because you need to lock in stone the actual details at time of transaction not what they are now!. A user could change their address at any time, a product can and will change price and description over time etc…. I’m really appalled to see such a good write up making such basic errors. So if anyone is reading this and looking for a solution, when creating payments make “copies” of all relevant transaction details (purchaser, seller, product, price, vat etc…) and DO NOT use entity relationships at all

  • Tino

    How to get data from IPN post message

  • http://webtempest.com Web Tempest

    Thanks Jamesw. I was kind of writing this as I was learning the paypal API – obviously did not think through the modeling and importance of keeping exact records of each transaction. I actually moved on to another project as this one fell through. Anyway will update the post in case I indirectly lead someone to jail

  • siva

    OpenSSL::PKey::RSAError
    in PaymentController#checkout

    Neither PUB key nor PRIV key:: nested asn1 error

  • http://webtempest.com Web Tempest

    Is this with my exact code? Unfortunately I stopped work on this and thus would appreciate it if you could write back when you find a solution.