Using Paypal with Rails
posted by vdimos 32 commentsIn this article:
For our latest joint venture we need to implement some kind of payment gateway. The requirements were simple:
- We need it secure
- We need it simple
- We need it now
The project was a complete overhaul of a job posting site : www.freshwebjobs.com
The talented folks over at Extendio had done a very nice job reskinning the site, and they wanted us to update the codebase, add RoR hype ,some new hooks and features.
The first thing that you think of when doing RoR and e-Commerce is Shopify. Shopify was created by jadedPixel. They extracted their accumulated e-shopping wisdom and gave the community Active Merchant. AM does a great job in abstracting payment gateway complexity and allowing you to use and switch different payment gateways. Our first choice for a payment gateway was AuthorizeNet. The whole RoR community seems to be using it, and their rates were quite acceptable.
The only problem is they work only with US-based companies.
Apparently most of the payment gateways supported by AM do.
After some research we stumbled upon “PayPal Website Payments Standard” (PPWPS). It is the simplest form of payment services offered by Paypal, allowing you to accept credit card and paypal payments.
Ok here is how it works:
The customer lands on your payment page. You can set up encrypted buttons with different amounts. The customer clicks on a button and is redirected to a paypal page where he can use his credit card or paypal login to issue the payment. After doing so he is redirected to your site (or wherever you specify) while an asynchronous notification system lets you know that you have received a payment.
Getting started
In order to get started with PPWPS we need to set up an Sandbox account. Paypal is providing developers with a virtual sandbox where you can make transactions without real money changing hands. Before you can use the sandbox you need to create a developer account and log into it.
After you created your developer account make sure you create two test accounts for the sandbox. One should be a business account the other a simple client account.
When done try to log in at www.sandbox.paypal.com.
Ok we now have our developer account and two accounts for the sandbox.
Paypal requires you to be logged into your developer account when trying to access the sandbox.
As a tip log with one of your browsertabs into your developer account and since the session timeouts pretty quick use autoreload, to reload the index page every 5 minutes.
Initial Setup
First thing we need to make sure is our application is playing in test mode. Test mode means that instead of using the original paypal infrastructure all request are made to the sandbox.
In your config/environment.rb file add the following lines:
1 2 # Ensure the gateway is in test mode 3 ActiveMerchant::Billing::Base.gateway_mode = :test 4 ActiveMerchant::Billing::Base.integration_mode = :test 5 ActiveMerchant::Billing::PaypalGateway.pem_file = 6 File.read(File.dirname(__FILE__) + '/../paypal/paypal_cert.pem')
The above code will make sure AM is in test mode. Also it will include the paypal certificate.
Certificates
To get the paypal certificate and setup your own, we will need to log into the sandbox with your business test account.
The paypal sandbox can be somehow slow. Patience young jedi, patience…
When logged go to your “Profile” – “Encrypted Payment Settings”. Here download the paypal public certificate and store it in a folder in your application. I created a paypal folder in the root folder of the application. If you use something else make sure you update the references in environment.rb and crypto42.rb later on.
We will need to create some certificates ourselfes. If you are on a linux box do something like this:
1 2 penssl genrsa -out my-prvkey.pem 1024 3 4 then 5 6 openssl req -new -key my-prvkey.pem -x509 -days 365 -out my-pubcert.pem
In Ubuntu running the above without sudo throws some errors for me about beeing unable to use the random number generator. If you are not using Linux, consider using it :) .
The above should leave you with two files. my-prvkey.pem and my-pubcert.pem. Move both files into the paypal folder with the paypal public certificate.
Now return to your sandbox account and in “Your Public Certificates” upload your public certificate. After you added it you will see it in the listing with a Cert ID. Keep this Cert ID for later use.
Lib, controller and the view
Lets get down and write some code.
Here is a snippet I got from the internet and changed it a bit to fit my needs:
1 2 module Crypto42 3 class Button 4 def initialize(data) 5 my_cert_file = Dir.getwd + "/paypal/my-pubcert.pem" 6 my_key_file = Dir.getwd + "/paypal/my-prvkey.pem" 7 paypal_cert_file = Dir.getwd + "/paypal/paypal_cert.pem" 8 9 IO.popen("/usr/bin/openssl smime -sign -signer #{my_cert_file} -inkey #{my_key_file} -outform der -nodetach -binary | /usr/bin/openssl smime -encrypt -des3 -binary -outform pem #{paypal_cert_file}", 'r+') do |pipe| 10 data.each { |x,y| pipe << "#{x}=#{y}\n" } 11 pipe.close_write 12 @data = pipe.read 13 end 14 end 15 16 def self.from_hash(hs) 17 self.new hs 18 end 19 20 def get_encrypted_text 21 return @data 22 end 23 24 end #end button 25 end #end module
Simple save the above piece of code into a file in your applications lib directory. It basically calls the system openssl (make sure you have it installed) function to encrypt some data we pass in as argument.
We have following scenario for our site. The customer gets at some point to a page where we want to allow him to choose and buy between N distinguish options. These options will be buttons linking to Paypal. Upon clicking them the customer will be able to use Paypal to do the purchase.
Paypal lets you create buttons like this through their webpage, but these buttons are fixed and cannot be used to carry extra variables like maybe an invoice number.
We want to create such buttons ourselfes everytime a user comes to our payment site.
Here is how our users controller would look like:
1 2 class UsersController < ApplicationController 3 4 include ActiveMerchant::Billing::Integrations 5 require 'crypto42' 6 require 'money' 7 8 ... Different user functions... 9 10 #place order is for a specific job 11 def place_order 12 13 @job = Job.find(params[:job_id]) 14 fetch_decrypted(@job) 15 16 if @logged_user.credits > 0 17 render(:action => "confirm_order") 18 return 19 else 20 #place order will have our paypal buttons 21 render(:action => "place_order") 22 return 23 end 24 25 rescue ActiveRecord::RecordNotFound 26 flash[:alert] = "Buying credits for fun?" 27 redirect_to :action => "profile" 28 end 29 30 ... 31 32 private 33 def fetch_decrypted(job = nil) 34 35 # cert_id is the certificate if we see in paypal when we upload our own certificates 36 # cmd _xclick need for buttons 37 # item name is what the user will see at the paypal page 38 # custom and invoice are passthrough vars which we will get back with the asunchronous 39 # notification 40 # no_note and no_shipping means the client want see these extra fields on the paypal payment 41 # page 42 # return is the url the user will be redirected to by paypal when the transaction is completed. 43 decrypted = { 44 "cert_id" => "cert id from your paypal business account", 45 "cmd" => "_xclick", 46 "business" => "name@yourpaypal.com", 47 "item_name" => "FWJ - 1 Credit", 48 "item_number" => "1", 49 "custom" =>"something to pass to IPN", 50 "amount" => "75", 51 "currency_code" => "USD", 52 "country" => "US", 53 "no_note" => "1", 54 "no_shipping" => "1", 55 } 56 57 if job 58 decrypted.merge!("invoice" => "Another passthrough var", "return" => "http://www.freshwebjobs.com/users/done?job_id=#{job.id}") 59 else 60 decrypted.merge!("return" => "http://www.freshwebjobs.com/users/done") 61 end 62 63 @encrypted_basic = Crypto42::Button.from_hash(decrypted).get_encrypted_text 64 65 66 @action_url = ENV['RAILS_ENV'] == "production" ? "https://www.paypal.com/uk/cgi-bin/webscr" : "https://www.sandbox.paypal.com/uk/cgi-bin/webscr" 67 end
Now we have our encrypted button code so we can use it like this in the view:
1 2 <form action="<%= @action_url %>" method="post"> 3 <input type="hidden" name="cmd" value="_s-xclick" /> 4 <input type="hidden" name="encrypted" value="<%= @encrypted_basic %>" /> 5 <input type="image" src="/images/btn_buynow_SM.jpg" name="submit" alt="3 credits"> 6 </form>
@action_url is set by us depending on the mode.
IPN – Instant (?) Payment Notification
With all the above done, you should have a working payment site! Actually you could stop here and track the transactions via paypal. That would be rather weird though since the application would have no kind of automated feedback about the transactions. That is where IPN, Paypals’ automated asynchronous event notification system, comes into play. To set it up we need to enter the sandbox business account again, and go to “Profile” -> “Instant Payment Notification Preferences”. Turn it on and set the URL to a URL of you application we are going to use to handle IPNs.
Update: As Mathias mentions in the comments, the IPN URL can be passed as a separate button parameter to Paypal instead of hardcoding it as mentioned above
Here is the code fragment to handle the IPNs, pretty much as it is in the AM source code:
1 2 def ipn 3 # Create a notify object we must 4 notify = Paypal::Notification.new(request.raw_post) 5 6 #we must make sure this transaction id is not allready completed 7 if !Trans.count("*", :conditions => ["paypal_transaction_id = ?", notify.transaction_id]).zero? 8 # do some logging here... 9 end 10 11 12 if notify.acknowledge 13 begin 14 if notify.complete? 15 #transaction complete.. add your business logic here 16 else 17 #Reason to be suspicious 18 end 19 20 rescue => e 21 #Houston we have a bug 22 ensure 23 #make sure we logged everything we must 24 end 25 else #transaction was not acknowledged 26 # another reason to be suspicious 27 end 28 29 render :nothing => true 30 end 31
Everytime a user pays us, Paypal will issue a request to the IPN url appending a bunch of usefull information. AM is used to acknowledge the request (to make sure noone is spoofing them) and if everything is ok we can add the credits to the user.
Moving into production mode
In order to move our site into production mode we must not forget following things:
- Change :test to :production in environment.rb
- Download the real Paypal certificate from your real business account.
- Upload your own certificates to our account.
- Change the values for Cert_id, business_name, and returnURL in your button code in the controller
- Change the IPN URL in your business account profile.
- Additionaly you can check the allow only encrypted payments in your profile, and check the other settings as well.
To do some basic testing in the real world you can temporarily change the charged amount, make a purchase then issue a refund through paypal.
Ok I think I have covered the basics. Next post will be about testing IPN with mock objects.



Comments (32)
There's also a small-ish Paypal gem which only includes four classes but works just as well, including sandbox mode.
As a side note: You don't need to configure a specific IPN-URL in your Paypal profile. It does need to have one configured, but you can include it in the commands you send to Paypal (parameter is notify_url). It will be stored for each transaction made, and will be used for each subsequent IPN notification (if there are any more) for this transaction. This makes testing a little bit easier.
Thanks for the tip Mathias. I updated the post.
I've used your tutorial to get paypal ipn integration successfully working, thank you for sharing this information.
However, when the user is returned after the transaction they are logged out of my site. This is not happening every time though.
Do you know why this might be happening? I appreciate this is probably a paypal issue, but I just wondered if you might have come across it before or be able to point me in the right direction.
Hey Paul,
Glad you could use the information. We didn't have any issues like the ones you mention, but my first guess would be to check you are redirected to the same domain, and check the cookie with the session_id to see if it is correct.
Best of Luck
Great tutorial Vdimos !
i was suggest to add:
./script/plugin install http://activemerchant.googlecode.com/svn/trunk/active_merchant
for some reason installing active_merchant *gem* isnt worked (require "activemerchant" works but ActiveMerchant::Billing::Base.gateway_mode = :test doesnt)
Only installing plugin is solved problem ;(
Hey Alexey! Hope the tutorial was of some help. In fact to install the activemerchant gem the syntax is sudo gem install activemerchant. Best of luck, cheers Vasilis..
The crypto42.rb brings in the line: @data = pipe.read
the message: Loading 'screen' into random state -Loading 'screen' into random state - done
unable to write 'random state'
I have Vista running. Has anybody an idea how to fix the problem?
Hey anton,
I am pretty sure it is the syscall to the openssl binary, since it uses the random number generator of the underlying OS (i think). Since I don't have Vista, I can't really say much more, except to try the syscall from the command line yourself. Please let us know if you worked the issue out, so I can update the post. Best of Luck, Vasilis
@alexy
make sure you:
require "active_merchant" # not the underscore
Thanks for the tutorial! It was very useful. I did run into one problem. When clicking on my button I would get the following error from PayPal:
"We were unable to decrypt the certificate id."
If you see this problem, it's likely one of 2 things:
1 - You got the certs between the sandbox and the live site mixed up somehow. (this didn't happen to me, but while I was looking for a solution I found that this was often given as a reason)
2 - There are newline characters in the encrypted string. My form (for some unkown reason) was getting formed like so:
To fix it I just added the following to my controller:
@encrypted_basic = @encrypted_basic.gsub("\n","")
My last post isn't entirely correct :-S
While remove the newline characters from the encrypted field was required to get rid of the "decrypt the certificate id" issue, I began to get "email address for the business is not present in the encrypted blob" which means that something is going wrong with the encryption.
All the help seems to point at (again) 1 of 2 things:
- Wrong keys/certificates (I've check and check again)
- Some problem with encryption.
My testing has led nowhere so far, though I think this and the previous spacing thing are related. I've tried both on my windows machine (running local) and a test server running linux. :-( I'll post here if I figure it out.
If anyone's using FCKEditor, I was able to resolve the button error by viewing the source code (via the "view source" button in FCK) and backspacing content so it occurs side by side as opposed to on a new line. The new line was causing FCK to insert linebreaks () which broke the button code.
Thanks for the writeup. This was very very useful
Useful tutorial. And is that vim? Nice color scheme. What is it?
@John
You mean the color syntax thingy?
It's ultraviolet and radiograph
Ultraviolet: http://ultraviolet.rubyforge.org/
RadioGraph: http://agilewebdevelopment.com/plugins/radiograph
Thanks for the tutorial! I was puzzled over the encryption part til I came here =)
I'm still stuck on the "email address for the business is not present in the encrypted blob" error. What's weird is that everything works fine on my dev machine, but now I'm getting that error on the production server. Dev machine is OS X 10.5, production server is Ubuntu 6.06 (set up via Deprec). I think I've toggled everything needed for the production environment, but no love. Anyone have any ideas?
Thanks for this.
In order to get around the certificate decode errors I had to strip all newlines out of the encrypted data.
If you take line 63:
@encrypted_basic = Crypto42::Button.from_hash(decrypted).get_encrypted_text
And add a gsub to the end
.get_encrypted_text.gsub("\n","")
It should work.
First, thanks for the tutorial, vdimos, it helped me a lot :)
I ran into the "email address for the business is not present in the encrypted blob" error when I moved my code to my production server from my staging server.
It turned out that there was a difference in the version of openssl on the two machines. The newer version on the production machine (OpenSSL 0.9.8b 04 May 2006) was attempting to rewind the pipe.
This led to a truncated blob ... always 512 + 8 characters.
I figured this out by running the commands by hand.
For now, I'm writing the order out to a tmp file until I can find and build a version of openssl that doesn't exhibit the problem.
I can confirm that OpenSSL 0.9.7a doesn't suffer from the encryption issues.
Hey, i'm half way done, but i'd be remissed if i didn't pointout that you are missing the letter 'o' in your first openssl call. I did a google search for penssl and 'mac' cause i've never heard of it before... yes i'm an idiot. just wanted to help other idiots.
Hey guys,
i am having a problem with an e-mail address as it was already mentioned here in the comments, but i can't figure out how to deal with it. Can please someone direct me in a right direction.
"The email address for the business is not present in the encrypted blob. Please contact your merchant."
Thanks
Hello,
I can't figure out the IPN section, what is this "Trans" class? Where is it coming from?
Oops, please nevermind me. Trans is your own data object for transactions. I guess it is time to go to bed now. :)
Great job, guys.
Did 1 by 1 everything,
downloaded paypal certificate, generated .pem's with openssl, uploaded back public certificate, copied cert id from paypal to application, but paypal returns me:
"The email address for the business is not present in the encrypted blob. Please contact your merchant."
And offcourse in openssl i entered same email, as paypal account.
Paypal isnt replying for this.
Does someone encountered same problem ?
Maybe something with Crypto42 library, i am 3 days working with that, paypal isnt helps me much ...
Im exhausted.
I have empty @encrypted_basic, any ideas?
I was getting an InvalidAuthenticityToken error so I turned it off by added the following:
protect_from_forgery :except => [:ipn, :done ]
If there is a better work around for this issue?
are you master about this, paypal is my favorite payment processor
Useful tutorial thanks bro, I'll take a note on this
I'm starting to use rails and javascript / ajax combination.
This should give me an excellent idea.
many thanks friend :)
Drop a comment: