142: PayPal Notifications
(view original Railscast)
In our last episode we showed how to create a checkout process for your Rails e-commerce site with PayPal. By the end of it a customer could add items to their cart and click a ‘checkout’ list to be taken to the PayPal site. There the same items were shown and they could enter payment information and place an order. One problem we were left with was that after the customer had paid for their order and redirected back to our site the items they had bought were still in their cart. Our site had no way of knowing that the payment had been made so didn’t know when to clear the cart. What we needed was a notification from PayPal that the purchase has been made so that we could clear the cart.
The Checkout Process
Our checkout process starts on our server with the cart page, then transfers to PayPal’s server where the customer views their billing information, makes their purchase, views their receipt then finally redirects back to our site. The process takes place entirely on PayPal’s server and so we have no way of knowing how it went.
The solution to this is to use Instant Payment Notification. This is a service that PayPal provides to notify your web server of the status of a purchase, so we can know if a purchase was successful or not. It works by making an asynchronous call to our server from PayPal. This can create problems if the PayPal servers are running slowly, though it generally only takes a few seconds so by the time our customer returns to our site we should know the status of their transaction.
Creating The Payment Notification Handler
To use this service we have to create a URL to which PayPal can send its notifications. We’re going to create a whole new resource to handle this, called PaymentNotification
. That way every time PayPal sends a notification we can create a whole new model and store it in our database. This has an added benefit: as every notification is stored in the database we build up a log of notifications that we can look at if we need to see the history of our notifications
First, we’ll generate our model. We’ll explain the parameters below.
script/generate model PaymentNotification params:text status:string transaction_id:string cart_id:integer
The first parameter, params
, will be used to store all of the parameters that PayPal sends to us as a serialized hash. We’ll also store two of the parameters from PayPal in separate fields, the status of the transaction and the transaction’s id, as this will make looking up notification records easier. A Payment Notification will be tied to a specific Cart, so we’ll store the cart’s id, too.
class PaymentNotification < ActiveRecord::Base belongs_to :cart serialize :params after_create :mark_cart_as_purchased private def mark_cart_as_purchased if status == "Completed" cart.update_attributes(:purchased_at => Time.now) end end end
The PaymentNotification model.
The PaymentNotification
model has a cart_id
, so belongs_to
a cart. We want to serialize our parameters, so we’ll use the serialize
method which will convert the params hash to a YAML format string for storing in the database (and turn in back into a hash when we get it back from the database). We also have an after_create
filter that marks the cart as purchased by setting its purchased_at
attribute to the current time if the payment notification’s status is “Completed”.
Next we’ll generate the controller. The PaymentNotifications
controller will only have one action, create
, as that’s all we want to do for now.
class PaymentNotificationsController < ApplicationController protect_from_forgery :except => [:create] def create PaymentNotification.create!(:params => params, :cart_id => params[:invoice], :status => params[:payment_status], :transaction_id => params[:txn_id] ) render :nothing => true end end
The PaymentNotifications controller.
The create
action will be called by PayPal. Note that because of Rails’ built-in forgery protection we have to exempt the action from the forgery check. As mentioned above when we created the PaymentNotification
model, we’re storing all of the parameters sent by PayPal in one field, and some of the other parameters separately. A good guide to the parameters that PayPal sends can be found in the Order Management Integration Guide on PayPal’s website.
Finally we’ll update our database with rake db:migrate
and add the line
map.resources ’payment_notifications’
to our routes.db
file and we’re ready to go.
Changing The Cart
In our application controller we have a method that fetches the current cart. We’re going to alter it so that it is reset if the cart is marked as purchased.
def current_cart if session[:cart_id] @current_cart ||= Cart.find(session[:cart_id]) session[:cart_id] = nil if @current_cart.purchased_at end if session[:cart_id].nil? @current_cart = Cart.create! session[:cart_id] ||= @current_cart.id end @current_cart end
We’ve rewritten the current_cart
method so that if a cart currently exists for a user (i.e. there is a cart_id
in their session) we’ll check if that cart has been purchased. If it has we’ll remove it from the session. The second part of the method will then create a new cart for that user so that they can, if they wish, start again and buy more items from our shop.
The Last Step
There is one last thing to do. Although we’ve created the method for PayPal to use, we haven’t told them to use it. If you look back to the previous episode you’ll see that we pass a number of parameters to PayPal when we send them a cart. We can pass a notify_url
parameter to tell PayPal which URL to pass its notifications to. We’ll need to make a small change to our cart model to add this parameter to the paypal_url
model.
def paypal_url(return_url, notify_url) values = { :business => ’seller_1234111143_biz@asciicasts.com’, :cmd => ’_cart’, :upload => 1, :return => return_url, :invoice => id, :notify_url => notify_url } line_items.each_with_index do |item, index| values.merge!({ "amount_#{index + 1}" => item.unit_price, "item_name_#{index + 1}" => item.product.name, "item_number_#{index + 1}" => item.product.identifier, "quantity_#{index + 1}" => item.quantity }) end "https://www.sandbox.paypal.com/cgi-bin/webscr?" + values.map { |k,v| "#{k}=#{v}" }.join("&") end
The updated paypal_url method in the cart model.
As we don’t have access to the URL helpers in the model we’ll pass the notification URL in as a parameter. We’ll change the cart’s view page (/app/views/cart/show.html.erb
) to pass the payment notification parameter to the model.
<%= link_to "Checkout", @cart.paypal_url(products_url, payment_notifications_url) %>
Testing It All
Unfortunately, testing that the notification actually works is difficult when we’re in development mode as our notification URL will be on localhost
which, of course, PayPal won’t be able to send notifications to. To test our cart properly we’d have to put our site up on a staging server which has a publicly accessible URL. To quickly test our cart code we’ll first buy some items on our site and go through the purchase process on PayPal and make a note of our transaction id on the receipt page.
Secondly we’ll simulate the payment notification by using curl
.
curl -d "txn_id=9JU83038HS278211W&invoice=1&payment_status=Completed" http://localhost:3000/payment_notifications
Using curl
to simulate a payment notification.
To simulate the payment notification we need to pass through three parameters to our create
action: the transaction id from the receipt page, the cart’s id and the payment status. The simulated request should return no response but if we check our database there should now be a time in the purchased_at
field for that cart in the database. Now, when we click back through to our store and buy another item our cart will be empty.
What About Security?
There is still a lot to do with the application in the area of security. We’re currently sending the item names and prices in the query string, which is an obviously insecure way to do it as the prices could be changed by altering the values in it. The next episode in this series will focus on making our application’s payment process more secure.