homeASCIIcasts

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.

Copying the transaction id from the receipt page on the PayPal site.

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.