Paypal Express Checkout with Ruby on Rails and paypal-sdk-merchant
In my last work, Agrimè.it, an ecommerce built using Ruby on rails, I had to implement the cart payment using Paypal Express Checkout.
Sadly, I found that was the worst time to do it, because Paypal was migrating the classic API to the new REST API and the documentation was a real mess! Walking through the doc links, I jumped (without a particular logic) from the new to the old API reference and vice versa.
After a long work, me and Alessandro were able to create a working Ruby class to implement Express Checkout using the classi API and the paypal-sdk-merchant gem.
If you want to try the new REST API check this link http://paypal.github.io/ and use the paypal-sdk-rest gem. If you succeed in using it, please let me know or write something about it.
For non-US developers: before using the new REST API check if they work in your country. Only after a few days of work we found a little and tiny box which said the new API only work in US at that moment and they will be able worldwide in the late 2013!
Configure paypal-sdk-merchant
To use the paypal-sdk-merchant gem in a Rails app just add:
gem 'paypal-sdk-merchant'
in the Gemfile, then install with bundle install. You can generate the config/paypal.yml config file with
rails g paypal:sdk:install
You can find an example in https://github.com/paypal/merchant-sdk-ruby . To obtain the configuration values you must register the app both in the Paypal sandbox (for development use) and in Paypal Apps.
To integrate the gem with Rails, create a file config/initializers/paypal.rb with the content:
PayPal::SDK.load("config/paypal.yml", Rails.env)
PayPal::SDK.logger = Rails.logger
Now the Rails app is ready to go. But… where?? :)
Express checkout, how it works?
Paypal has a lot of different workflows. Before start hacking, I want to explain how the express checkout works.
It’s essentialy constituted by 3 steps:
- Set express checkout: makes a call to the API sending the details of the payment to obtain a token and generate a payment url
- Get express checkout: redirects the user to the payment url and obtains the authorization
- Do express checkout: uses the given authorization to make the real payment
Remember the third step. Without it you just ask for a payment authorization without ever getting it! To clearify these steps you can play with the sample app at https://paypal-sdk-samples.herokuapp.com/merchant/set_express_checkout
Code implementation
The paypal library file
I put the Paypal logic in the lib/modules/paypal_interface.rb file. Its basic structure is:
require 'paypal-sdk-merchant'
class PaypalInterface
attr_reader :api, :express_checkout_response
PAYPAL_RETURN_URL = Rails.application.routes.url_helpers.paid_orders_url(host: HOST_WO_HTTP)
PAYPAL_CANCEL_URL = Rails.application.routes.url_helpers.revoked_orders_url(host: HOST_WO_HTTP)
PAYPAL_NOTIFY_URL = Rails.application.routes.url_helpers.ipn_orders_url(host: HOST_WO_HTTP)
def initialize(order)
@api = PayPal::SDK::Merchant::API.new
@order = order
end
end
The @order variable is an instance of the Order model, which referers to the order going to be paid. I’ll use this model later to save the data needed to manage the payment.
Required routes
As you can see in the code below, the library needs three routes. This is the pertinent code from config/routes.rb:
resources :orders do
collection do
get :paid
get :revoked
post :ipn
end
end
I defined the HOST_WO_HTTP variable in a custom initializer because it differs depending on the environment, here it is:
if Rails.env.production?
HOST_WO_HTTP = 'www.agrime.it'
else
HOST_WO_HTTP = 'localhost:3000'
end
HOST = "http://#{HOST_WO_HTTP}"
I’m sure a better (and more railsy) way exists, but I didn’t find it. Any suggestion is welcome! :)
Get express checkout
Now it’s time do make the first step and request the url for the payment. Imagine we have an unpaid order and in its show action, we want to add a “Pay with Paypal” button.
This is the controller action:
def show
@order = Order.find(params[:id])
@paypal = PaypalInterface.new(@order)
@paypal.express_checkout
if @paypal.express_checkout_response.success?
@paypal_url = @paypal.api.express_checkout_url(@paypal.express_checkout_response)
else
# manage error
end
end
The main method used here is express_checkout, defined in the PaypalInterface class:
class PaypalInterface
[..]
def express_checkout
@set_express_checkout = @api.build_set_express_checkout({
SetExpressCheckoutRequestDetails: {
ReturnURL: PAYPAL_RETURN_URL,
CancelURL: PAYPAL_CANCEL_URL,
PaymentDetails: [{
NotifyURL: PAYPAL_NOTIFY_URL,
OrderTotal: {
currencyID: "EUR",
value: @order.total
},
ItemTotal: {
currencyID: "EUR",
value: @order.total
},
ShippingTotal: {
currencyID: "EUR",
value: "0"
},
TaxTotal: {
currencyID: "EUR",
value: "0"
},
PaymentDetailsItem: [{
Name: @order.code,
Quantity: 1,
Amount: {
currencyID: "EUR",
value: @order.total
},
ItemCategory: "Physical"
}],
PaymentAction: "Sale"
}]
}
})
# Make API call & get response
@express_checkout_response = @api.set_express_checkout(@set_express_checkout)
# Access Response
if @express_checkout_response.success?
@order.set_payment_token(@express_checkout_response.Token)
else
@express_checkout_response.Errors
end
end
end
The Order.set_payment_token method just saves the token in the order model. It will be used after the payment, to link the payment with the correct order.
At the end of the API call you can use the @paypal_url variable to build the “Pay with Paypal” button. This is the view:
<%= link_to @paypal_url do %>
<img src="https://www.paypalobjects.com/it_IT/IT/Marketing/i/bnr/bnr_horizontal_solutiongraphic_335x80.gif" style="margin-right:7px;" />
<% end %>
Clicking in the button the user will be redirected to Paypal, where he can pay using it’s account or a credit card.
Get express checkout
If the user accepts the payment, Paypal redirects to the PAYPAL_RETURN_URL passing two arguments: the token and the PayerID. The OrdersController.paid method is simple:
class OrdersController < ApplicationController
[..]
def paid
if order = Order.pay(params[:token], params[:PayerID])
# success message
else
# error message
end
redirect_to orders_path
end
end
The following is the Order.pay method:
class Order < ActiveRecord::Base
[..]
def self.pay(token, payerID)
begin
order = self.find_by_payment_token(token)
order.payerID = payerID
order.save
PaypalWorker.perform_async(order.id)
return order
rescue
false
end
end
end
The code finds the correct order using the token and saves the PayerID. Now the payment is just authorized. To obtain the real payment we must proceed with the third step. I use a Sidekiq worker to perform this action asynchronously while the user is immediatly redirected to the orders page.
Before proceeding to the last step let’s check the revoke action. Paypal redirects to the PAYPAL_CANCEL_URL if the user doesn’t authorize the payment and clicks in the “Cancel” button.
class OrdersController < ApplicationController
[..]
def revoked
if order = Order.cancel_payment(params)
# set a message for the user
end
redirect_to orders_path
end
end
The params[:token] param let you find the canceled order. I do nothing particular with the other params, just save it for logging.
Do express checkout
Now that the payment is authorized we must obtain the real payment. As mentioned before I use an asyncronous job to make it, just to speed up the user experience. The worker code is really simple:
class PaypalWorker
include Sidekiq::Worker
def perform(order_id)
@order = Order.find(order_id)
@paypal = PaypalInterface.new(@order)
@paypal.do_express_checkout
end
end
Let’s jump to the PaypalInterface.do_express_checkout method:
class PaypalInterface
[..]
def do_express_checkout
@do_express_checkout_payment = @api.build_do_express_checkout_payment({
DoExpressCheckoutPaymentRequestDetails: {
PaymentAction: "Sale",
Token: @order.payment_token,
PayerID: @order.payerID,
PaymentDetails: [{
OrderTotal: {
currencyID: "EUR",
value: @order.total
},
NotifyURL: PAYPAL_NOTIFY_URL
}]
}
})
# Make API call & get response
@do_express_checkout_payment_response = @api.do_express_checkout_payment(@do_express_checkout_payment)
# Access Response
if @do_express_checkout_payment_response.success?
details = @do_express_checkout_payment_response.DoExpressCheckoutPaymentResponseDetails
@order.set_payment_details(prepare_express_checkout_response(details))
else
errors = @do_express_checkout_payment_response.Errors # => Array
@order.save_payment_errors errors
end
end
end
Essentially the method sends to paypal the token, the PayerID and the order details. If the values are correct, Paypal will make the real payment and will return a successful response. In addition it will make a POST call to the IPN url (we’ll see this below).
If something goes wrong we’ll get an error array like this:
[{
:ShortMessage => "This Express Checkout session has expired.",
:LongMessage => "This Express Checkout session has expired. Token value is no longer valid.",
:ErrorCode => "10411",
:SeverityCode => "Error"
}]
Error codes and details are documented here: https://developer.paypal.com/webapps/developer/docs/classic/api/errorcodes/
If the payments is correct, the method calls Order.save_payment_details, saving the payment details (I use a Payment model with an has_one association with Order).
IPN
After the express checkout, Paypal send a POST call to the IPN url:
class OrdersController < ApplicationController
[..]
def ipn
if payment = Payment.find_by_transaction_id(params[:txn_id])
payment.receive_ipn(params)
else
Payment.create_by_ipn(params)
end
end
end
The action tries to find the payment using the transaction ID and sends to it the params. If it doesn’t find it (and this is strange!) it creates a Payment saving the params (about payments, I prefer to log everything!). The IPN params are really a lot (check them here), I saves only a few of them in dedicated columns, then I put a raw copy of the params (using params.to_s ) in a text field for debugging porpouses.
Conclusion
I showed the basic code to implement Paypal Express Checkout using the paypal-sdk-merchant gem. The Paypal API is complex but the gem is quite simple to use, I hope the code is easy to understand. If you want to use it in a real app, I suggest to add some methods to manage the order status (new, paid, etc.) and to notify the user of what’s happening in the background.
Take a look at the webography below. When I started this project a few months ago, due to the migration to the new Paypal Developers Platform, all the documentation was quite unreadable. I reread some docs now while writing this post and I must admit the Paypal guys made a big step forward, probably your experience will be easier than mine :)
Thanks to Alessandro for his great help and good code for this project! :)
Webography
Some of the links used to write the code and this post:
CLASSIC API
- https://github.com/paypal/merchant-sdk-ruby
- https://developer.paypal.com/webapps/developer/docs/api/
- https://developer.paypal.com/webapps/developer/docs/classic/lifecycle/apps101/
- https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNandPDTVariables/
- http://www.paypalobjects.com/en_US/ebook/PP_ExpressCheckout_IntegrationGuide/HowExpressCheckoutWorks.html
- https://developer.paypal.com/webapps/developer/docs/classic/api/errorcodes/
REST API
- http://paypal.github.io/
- https://github.com/paypal/rest-api-sdk-ruby
- https://developer.paypal.com/webapps/developer/docs/api/#payments
CODE SAMPLES