While building an online store for one of my past projects using Rails and the Refinery CMS, I was presented with an interesting problem. I had a requirement to allow customers to purchase and download individual music tracks directly from an online store that also sells physical goods. On its surface, this sort of thing does not seem much different from selling tangible goods via the web: User selects items, places them in his/her cart, and checks out. After payment processing, the only thing different in the case of digital downloads would be in actually fulfilling the order.
The problem is not in getting the purchased tracks to the customer. It’s easy enough to email a link to the purchased files. However, a permanent link to your digital downloads would allow anyone to grab the files you’re trying to sell, for free. So, clearly a better solution is needed that will deny everyone but paying customers access to downloadable products.
A thought briefly occurred to me to just email the purchased tracks to the customer after a purchase was made. But that’s head-smackingly stupid for a number of reasons, not the least of which is the fact that many email providers place a size limit on attachments that would prevent this solution from working. Plus, what if a customer loses his emailed copy and wants to get the files he payed for a second time? Sure, you could tell him/her to just buy them again, but that’s not the kind of customer service that will engender loyalty and/or referrals.
A better solution would be to provide each customer with an access code that will allow him/her to access a download page containing all of the files purchased as part of a particular order. The Refinery CMS does a decent job of preventing unauthorized access to resources (our downloadable files, for example) that aren’t meant to be public, so a good amount of the work has already been done. However, one requirement of this project introduced another problem: PayPal was being used for order processing. The issue with using a third-party payment processor like PayPal is that you can’t know for sure if a customer’s payment was processed successfully after being forwarded from the main store.
Fortunately, PayPal provides a service it calls Instant Payment Notification (IPN). Basically, it works like this:
1.) A customer is forwarded to PayPal from your website to complete his purchase.
2.) Paypal processes the payment, and then sends a notification to a listener you’ve set up on your site
3.) The listener verifies that the payment notification is valid by sending it, unaltered, to PayPal
4.) If the notification is legit, PayPal sends back a final, one-word response, either INVALID or VERIFIED.
The IPN also contains all the information forwarded to PayPal when transferring the user from your site, along with some details provided to PayPal, such as the customer’s email address. So, if you set up your store correctly you should receive everything you need to fulfill the order.
So, armed with this information, the final process for digital download sales is as follows:
1.) Customer purchases files via PayPal
2.) PayPal sends an IPN to the website’s listener
3.) The listener verifies the IPN.
4.) If verified, the listener generates an access code and sends an email to the customer containing a link to a download page.
Setting up a listener is easy if you use ActiveMerchant, as it includes built in support for PayPal. Here’s an example using a simple five digit number for an access code (not the most secure thing in the world, you might want to use a longer string that can consist of the full gamut of alphanumeric characters). We hash the access code using MD5 and store the hash in the database. Here’s an example controller for a PaymentNotification model:
class PaymentNotificationsController [:create] def create notify = Paypal::Notification.new(request.raw_post) Rails.logger.info "In create" #Verify IPN with PayPal if notify.acknowledge Rails.logger.info "Acknowledge" if notify.complete? Rails.logger.info "payment complete" notification = PaymentNotification.create!(:params => params, :cart_id => params[:invoice], :status => params[:payment_status], :transaction_id => params[:txn_id]) Cart.find(params[:invoice]).update_attribute(:purchased_at, Time.now) #Generate access code random_number_generator = Random.new random = random_number_generator.rand(10000...99999) hashed_random = Digest::MD5.hexdigest(random.to_s) #store access code in database notification.update_attribute(:access_code, hashed_random) #Send Email notification OrderNotifier.received(notification, random).deliver else Rails.logger.error("Failed to verify Paypal's notification") end end render :nothing => true end end
And if you’re curious about the PaymentNotification model itself, it’s pretty simple:
class PaymentNotification :create validates_presence_of :cart_id, :on => :create validates_uniqueness_of :transaction_id validates_uniqueness_of :cart_id serialize :params end
The data we need is pulled from the IPN itself (contained in :params). The cart_id is the id number of a cart in the site’s database that contains the customer’s order. This was forwarded to PayPal when the customer checked out and is returned as params[:invoice]. The :transaction_id (returned as params[:txn_id]) is a unique identifier assigned to the order by PayPal.
OrderNotifier is just a child of ActionMailer, and it’s used to send the transaction_id and access code to the customer:
class OrderNotifier def received(notification, random) @notification = notification @code = random @greeting = "Hello" @cart = Cart.find(@notification.cart_id) mail :to => @notification.params[:payer_email] #mail :to => "firstname.lastname@example.org" end end
The email sent to the customer would contain a section that goes something like this:
To get your files, please visit the following link and enter your transaction ID and access code: http://www.example.com/carts/ Transaction ID : Access Code :
When the customer visits the provided link, he’s presented with a form requesting the transaction ID and access code:
Entering the correct information would then present the user with a page containing links to the downloads he ordered. There are numerous variations on this basic system one could do. For example, with a few small modifications one could easily limit the number of times a user could download his/her files to, say, five. There are also third party solutions available to manage digital download sales, though I haven’t tried any yet.