Integrating Paypal button with IPN and PDT in ASP.net or MVC website

I was working on a project where a subscription is needed to access parts of the website (drivingtest.cleverdodo.mu). The idea was to have a Paypal button which would allow users to pay for 1 month subscription at a time and give them instant access. The standard button (the one you get from Paypal’s website) allows you to receive money but you’d have to manually check the payment and update the customer’s record to grant him access. If you wanted your system to be notified instantly, you’ll have to dive into Instant Payment Notification (IPN) and Payment Data Transfer (PDT).

Before I show you the C# codes for Paypal integration, you’ll need to understand what IPN and PDT are. IPN is a message service; when someone has made a payment to you, Paypal will send this message to your listener. It is the most reliable way to know whether you truly have received money because the message will be sent at some point or the other, whether the user does not return to your website after completing the payment or he loses internet connection or there’s congestion and the transaction notification is delayed. However you’ll need to have a listener to capture the message. PDT on the other hand happens when the user either clicks on the “Goc back to merchant website” link (your return url) or when the user is redirected to your website immediately after purchase. If for some reason or the other, the customer does not return to your website, you will receive no notification of a successful payment.

Therefore it is important that you have both IPN and PDT but the good news is that the codes for the two are not that much different after all.

Paypal Sandbox and the Developer account

You do not want to be testing against your live business/personal account. Therefore you’ll need to register for a developer account first to get access to Paypal Sandbox which allows you to test at your heart’s will.

Developer : https://developer.paypal.com/

Once registered, just create a test business account and a personal account so that you can check whether when you buy from the personal account, the money is transferred to your business account.

You’ll also need to sign in with your test business account to https://www.sandbox.paypal.com and go to Profile -> Website Payment Preferences and turn Auto Return and Payment Data Transfer to On so that you get an Identity Token to use for PDT. I had trouble finding my token because I was looking on the Developers url rather than the Sandbox one.

How it works in my project

When a user is logged in, he’s able to see a Paypal button in his dashboard (my account area). When he clicks the button, it takes him to an intermediate page which contains a form with all the necessary values to send off to Paypal. When this page is loaded, a record is created in the order table and the order id is inserted as a hidden form field. I need this value back from Paypal to update the corresponding user subscription record.

So this is an MVC view but the core principle remains the same for any other programming language


@{
 ViewBag.Title = "Paypal Checkout";
}

@section TestHeadContent {
<meta name="robots" content="noindex" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
<script src="/scripts/jquery.countdown.min.js"></script>
<script type="text/javascript">
 $(document).ready(function () {
 $('#paypal-timer').countdown({ until: '+2s', format: 'MS', layout: '{sn}', onExpiry: SubmitTheForm });
 });

function SubmitTheForm() {
 $('#paypalform').submit();
 }
</script>
}

<div id="area">
<div style="min-height:400px;">
<strong>Redirecting you to the Paypal website in <span id="paypal-timer"></span> second(s)...</strong>
<form name="paypalform" id="paypalform" action="@ViewData["PaypalUrl"]" method="post" enctype="application/x-www-form-urlencoded">
 <input type="hidden" name="cmd" value="_xclick" />
 <input type="hidden" name="business" value="@System.Configuration.ConfigurationManager.AppSettings["Business"]" />
 <input type="hidden" name="amount" value="4" />
 <input type="hidden" name="currency_code" value="USD" />
 <input type="hidden" name="notify_url" value="@String.Concat(System.Configuration.ConfigurationManager.AppSettings["WebsiteUrl"], "paypal/ipn")" />
 <input type="hidden" name="cancel" value="@String.Concat(System.Configuration.ConfigurationManager.AppSettings["WebsiteUrl"], "paypal/cancel")" />
 <input type="hidden" name="return" value="@String.Concat(System.Configuration.ConfigurationManager.AppSettings["WebsiteUrl"], "paypal/pdt")" />
 <input type="hidden" name="item_name" value="1 Month Access" />
 <input type="hidden" name="custom" value="@ViewData["OrderId"]" />
</form>
</div>
</div>

The form is populated with values from the AppSettings; Business is your business email address and notify_url is the URL of the IPN listener. The form action is set to either the sandbox or live Paypal url depending on a setting in the web.config file. After 2 seconds, the form is automatically submitted.

Note : You cannot issue a post command from code behind (ie programmatically) because you will need to redirect the user to that same page afterwards for him to enter his details. Therefore you will lose your post data. That is why we’re using a form to post the values and send the users to Paypal.

Code for IPN

Usually this code will go into your business logic layer (thin controllers, fat models) but this is just to simplify things.


public ActionResult IPN()
 {
 log.Info("IPN listener invoked");

try
 {
 var formVals = new Dictionary<string, string>();
 formVals.Add("cmd", "_notify-validate");

string response = GetPayPalResponse(formVals);

if (response == "VERIFIED")
 {
 string transactionId = Request["txn_id"];
 string sAmountPaid = Request["mc_gross"];
 string orderId = Request["custom"];

//_logger.Info("IPN Verified for order " + orderID);

//validate the order
 Decimal amountPaid = 0;
 Decimal.TryParse(sAmountPaid, out amountPaid);

Order order = new Gis.Quiz.Core.Repository.Repository<Order>(_session).GetById(Convert.ToInt32(orderId));

// check these first
 bool verified = true;

string businessEmail = HttpUtility.UrlDecode(Request["business"]);
 if (String.Compare(businessEmail, System.Configuration.ConfigurationManager.AppSettings["Business"], true) != 0)
 verified = false;

string currencyCode = HttpUtility.UrlDecode(Request["mc_currency"]);
 if (String.Compare(currencyCode, "USD", true) != 0)
 verified = false;

string paymentStatus = HttpUtility.UrlDecode(Request["payment_status"]);
 if (String.Compare(paymentStatus, "Completed", true) != 0)
 verified = false;

if (!AmountPaidIsValid(order, amountPaid))
 verified = false;

log.Info("Business : " + businessEmail);
 log.Info("currency : " + currencyCode);
 log.Info("payment status : " + paymentStatus);
 log.Info("amount valid : " + AmountPaidIsValid(order, amountPaid).ToString());

// process the transaction
 if (verified)
 {
 log.Info("Trying to process ipn transaction...");

// check that we have not already processed this transaction
 if (String.Compare(order.TransactionId, transactionId, true) != 0)
 {
 order.Firstname = HttpUtility.UrlDecode(Request["first_name"]);
 order.Lastname = HttpUtility.UrlDecode(Request["last_name"]);
 order.Email = HttpUtility.UrlDecode(Request["payer_email"]);
 order.Street = HttpUtility.UrlDecode(Request["address_street"]);
 order.City = HttpUtility.UrlDecode(Request["address_city"]);
 order.State = HttpUtility.UrlDecode(Request["address_state"]);
 order.Country = HttpUtility.UrlDecode(Request["address_country"]);
 order.Zip = HttpUtility.UrlDecode(Request["address_zip"]);
 order.TransactionId = transactionId;
 order.StatusId = 1;
 order.UpdateDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now);

UserSubscription us = new Gis.Quiz.Core.Repository.Repository<UserSubscription>(_session).GetById(order.UserId);
 if (us == null)
 {
 // create new subscription record
 us = new UserSubscription();
 us.Id = order.UserId;
 us.CreateDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now);
 us.UpdateDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now);
 us.SubscriptionEndDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now.AddDays(30));
 }
 else
 {
 // update subscription record
 us.UpdateDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now);
 us.SubscriptionEndDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now.AddDays(30));
 }

using (ITransaction tx = _session.BeginTransaction())
 {
 _session.Save(order); // save order
 _session.Save(us); // save subscription

tx.Commit();
 }
 }
 }
 else
 {
 //let fail - this is the IPN so there is no viewer
 log.Info("ipn is not valid...");
 }
 }
 else
 log.Info("ipn not verified...");
 }
 catch (Exception ex)
 {
 log.Error("Error caught in IPN : " + ex.ToString());
 }

return View();
 }

You need to check you haven’t processed the transaction yet because you could end up doing it twice as Paypal could send you a notification through both PDT and IPN.

The PDT Code


public ActionResult PDT()
 {
 log.Info("PDT called...");

try
 {
 //_logger.Info("PDT Invoked");
 string transactionId = Request.QueryString["tx"];
 string sAmountPaid = Request.QueryString["amt"];
 string orderId = Request.QueryString["cm"];

Dictionary<string, string> formVals = new Dictionary<string, string>();
 formVals.Add("cmd", "_notify-synch");
 formVals.Add("at", System.Configuration.ConfigurationManager.AppSettings["PayPalPDTToken"]);
 formVals.Add("tx", transactionId);

string response = GetPayPalResponse(formVals);
 //_logger.Info("PDT Response received: " + response);
 if (response.StartsWith("SUCCESS"))
 {
 //_logger.Info("PDT Response received for order " + orderID);

//validate the order
 Decimal amountPaid = 0;
 Decimal.TryParse(sAmountPaid, out amountPaid);

Order order = new Gis.Quiz.Core.Repository.Repository<Order>(_session).GetById(Convert.ToInt32(orderId));

//check the amount paid
 if (AmountPaidIsValid(order, amountPaid))
 {
 // check that we have not already processed this transaction
 if (String.Compare(order.TransactionId, transactionId, true) != 0)
 {
 order.Firstname = GetPDTValue(response, "first_name");
 order.Lastname = GetPDTValue(response, "last_name");
 order.Email = GetPDTValue(response, "payer_email");
 order.Street = GetPDTValue(response, "address_street");
 order.City = GetPDTValue(response, "address_city");
 order.State = GetPDTValue(response, "address_state");
 order.Country = GetPDTValue(response, "address_country");
 order.Zip = GetPDTValue(response, "address_zip");
 order.TransactionId = transactionId;
 order.StatusId = 1;
 order.UpdateDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now);

UserSubscription us = new Gis.Quiz.Core.Repository.Repository<UserSubscription>(_session).GetById(order.UserId);
 if (us == null)
 {
 // create new subscription record
 us = new UserSubscription();
 us.Id = order.UserId;
 us.CreateDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now);
 us.UpdateDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now);
 us.SubscriptionEndDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now.AddDays(30));
 }
 else
 {
 // update subscription record
 us.UpdateDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now);
 us.SubscriptionEndDate = Gis.Quiz.Core.Utilities.Helper.FormatNHibernateDateTime(DateTime.Now.AddDays(30));
 }

using (ITransaction tx = _session.BeginTransaction())
 {
 _session.Save(order); // save order
 _session.Save(us); // save subscription

tx.Commit();
 }

ViewData["fb"] = "Thank you for your payment. Your transaction has been completed and a receipt for your purchase has been emailed to you. You can now access the paid members area of our website by <a href=\"/myaccount\">clicking here</a>.";
 }
 else
 {
 ViewData["fb"] = "This order has already been processed.";
 }
 }
 else
 {
 //Payment amount is off, this can happen if you have a Gift cert at PayPal, be careful of this!
 ViewData["fb"] = "Payment amount received from paypal does not match order total.";
 }
 }
 else
 {
 ViewData["fb"] = "Your payment was not successful with PayPal";
 }
 }
 catch (Exception ex)
 {
 log.Error("Error caught in PDT : " + ex.ToString());
 }

return View();
 }

PayPalPDTToken is the Identity Token you got in an earlier step above.

The helper methods


/// <summary>
 /// Utility method for handling PayPal Responses
 /// </summary>
 string GetPayPalResponse(Dictionary<string, string> formVals)
 {
 bool useSandbox = Convert.ToBoolean(System.Configuration.ConfigurationManager.AppSettings["IsLocal"]);

string paypalUrl = useSandbox ? "https://www.sandbox.paypal.com/cgi-bin/webscr"
 : "https://www.paypal.com/cgi-bin/webscr";

 HttpWebRequest req = (HttpWebRequest)WebRequest.Create(paypalUrl);

// Set values for the request back
 req.Method = "POST";
 req.ContentType = "application/x-www-form-urlencoded";

byte[] param = Request.BinaryRead(Request.ContentLength);
 string strRequest = Encoding.ASCII.GetString(param);

StringBuilder sb = new StringBuilder();
 sb.Append(strRequest);

foreach (string key in formVals.Keys)
 {
 sb.AppendFormat("&{0}={1}", key, formVals[key]);
 }
 strRequest += sb.ToString();
 req.ContentLength = strRequest.Length;

//for proxy
 //WebProxy proxy = new WebProxy(new Uri("http://urlort#");
 //req.Proxy = proxy;
 //Send the request to PayPal and get the response
 string response = "";
 using (StreamWriter streamOut = new StreamWriter(req.GetRequestStream(), System.Text.Encoding.ASCII))
 {

streamOut.Write(strRequest);
 streamOut.Close();
 using (StreamReader streamIn = new StreamReader(req.GetResponse().GetResponseStream()))
 {
 response = streamIn.ReadToEnd();
 }
 }

return response;
 }

bool AmountPaidIsValid(Order order, decimal amountPaid)
 {
 bool result = true;

if (order != null)
 {
 if (order.Total > amountPaid)
 {
 //_logger.Warn("Invalid order amount to PDT/IPN: " + order.ID + "; Actual: " + amountPaid.ToString("C") + "; Should be: " + order.Total.ToString("C") + "user IP is " + Request.UserHostAddress);
 result = false;
 }
 }
 else
 {
 //_logger.Warn("Invalid order ID passed to PDT/IPN; user IP is " + Request.UserHostAddress);
 }
 return result;
 }

string GetPDTValue(string pdt, string key)
 {
 string[] keys = pdt.Split('\n');
 string thisVal = "";
 string thisKey = "";
 foreach (string s in keys)
 {
 string[] bits = s.Split('=');
 if (bits.Length > 1)
 {
 thisVal = HttpUtility.UrlDecode(bits[1]); // values are urlencoded, not htmlencoded
 thisKey = bits[0];
 if (thisKey.Equals(key, StringComparison.InvariantCultureIgnoreCase))
 break;
 }
 }
 return thisVal;
 }

Important things to remember

Here are some thing worth noting:

  • You need to be logged into your sandbox account on the same browser that you’re sending off the request to Paypal to make a sucessful transaction in testing mode.
  • You can only test IPN on a URL that’s publicly accessible. So localhost will not work.

Paypal integration is not complicated but I had to read lots on different forums/blogs and StackOverflow and I’m grateful to Kona.Web from which I’ve borrowed a lot of code.

Displaying your recent posts in Tumblr

Tumblr does not provide an easy way to show your latest posts on your blog. The infinite scrolling is nice but you want your visitors to be able to see say the last 5 posts you’ve made so they can jump straight to a specific post instead of having to read through all posts until they reach something worth reading. So to achieve this functionality, I’ve used Jquery/JavaScript.

There are 3 things to accomplish this:

  1. If you’re not already using the jquery library, you need to include it in your theme.
  2. Insert an html ul tag where you want the posts to render.
  3. Add the jQuery code to your theme.

Some themes already come with jquery and therefore there’s no need to add it again. However the library needs to appear before the jQuery code for it to work. Check for the word jquery in your theme’s html and if you can’t find any instances of it, then add it in the head section, that is within these tags <head></head>. Then right after it, place the jQuery code. So you will have the following:


<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>

<script type="text/javascript">
$(function() {
var url = 'http://staff.tumblr.com/api/read?start=0&num=5';
var $list = $('#recent-posts');
$.ajax({
 url: url,
 type: 'GET',
 dataType: 'xml',
 success: function(data) {
 var posts = $(data).find('post');
 posts.each( function() {
 var post = $(this);
 var link = post.attr('url-with-slug');
 var title = post.children('regular-title').text();
 if (link && title) {
 $list.append('<li><a href="' + link + '">' + title + '</a></li>');
 }
 });
 }
});
});
</script>

Replace staff.tumblr.com with the hostname of your blog and if you want to change how many posts are displayed, then change the number 5 at the end of the url eg http://staff.tumblr.com/api/read?start=0&num=20. The latter will retrieve the last 20 blog posts.

Now you need to include the following somewhere in your theme where you want the posts to be displayed:


<ul id="recent-posts">
 </ul>

You might want to place that in the sidebar so that it’s prominent. And that’s it really.

Any problems, let me know 🙂