Integrating IPNs Into Tweet Relevance

While the previous sections regurgitated much of the introductory content in the Instant Payment Notification Guide and created a necessary foundation, it’s all theory until rooted in some sample project code. This section creates a GAE project that implements an IPN listener and teaches you how to use it to perform post-processing actions with the Tweet Relevance sample project from Chapter 2, which involved an Express Checkout integration. The specific post-processing action that we’ll perform is to send a follow-up email to the purchaser of a Tweet Relevance subscription after verifying that the IPN originated with PayPal. Although conceptually simple, this approach provides a realistic yet isolated view of what an IPN listener implemented with GAE might look like and hopefully will serve you well as a jumping off point—not to mention that it demonstrates how to send mail through GAE.

Note

Another obvious option for employing IPNs as part of a Tweet Relevance payment scenario is to use them to take a specific action when an eCheck status is successfully resolved. A recommended exercise for this chapter suggests a scenario involving IPNs and eChecks.

An austere template for interacting with IPNs and performing a custom action based on information provided by the IPN is given in Example 6-1, which shows how you could use an IPN to send a follow-up email message to the payer about her purchase. It does not persist information such as the transaction ID or perform other important implementation details, such as customizing the message based upon whether the payment status is Completed. The only change that should be necessary in order to deploy the code to your live GAE environment on AppSpot is specifying an email address for the gae_email_sender that is registered with your GAE account, as specified in the Administration/Permissions section as shown in Figure 6-2.

Example 6-1. main.py—a basic template for handling an IPN from PayPal and sending a mail message in response to it as a custom action

# Use the IPN Simulator from the "Test Tools" at developer.paypal.com to
# send this app an IPN. This app must be deployed to the live GAE environment
# for the PayPal IPN Simulator to address it. When debugging, it's a necessity
# to use logging messages and view them from the GAE Dashboard's Logs pane.

# You can also login to a Sandbox Merchant account and set the IPN URL for
# this app under the Merchant Profile tab.

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.api import urlfetch
from google.appengine.api import mail

import cgi

import logging

# In production, gae_email_sender must be an authorized account that's been
# added to the GAE Dashboard under Administration/Permissions or else you
# won't be able to send mail. You can use the default owner of the account
# or add/verify additional addresses

gae_email_sender = "XXX"
ipn_sandbox_url = "https://www.sandbox.paypal.com/cgi-bin/webscr"

class IPNHandler(webapp.RequestHandler):

    @staticmethod
    def sendMail(first_name, last_name, email, debug_msg=None, bcc=None):
        message = mail.EmailMessage(sender="Customer Support <%s>" % (gae_email_sender,),
                                    subject="Your recent purchase")

        message.to = "%s %s <%s>" % (first_name, last_name, email,)

        message.body = """Dear %s:

Thank you so much for your recent purchase.

Please let us know if you have any questions.

Regards,
Customer Support""" % (first_name,)

        if debug_msg:
            message.body = message.body + '\n' + '*'*20 + '\n' + debug_msg + '\n' + '*'*20

        if bcc:
            message.bcc = bcc

        message.send()


    def post(self, mode=""):

        # PayPal posts to /ipn to send this application an IPN

        if mode == "ipn":

            logging.info(self.request.body)
           
            # To ensure that it was really PayPal that sent the IPN, we post it back
            # with a preamble and then verify that we get back VERIFIED and a 200 response

            result = urlfetch.fetch(
                        ipn_sandbox_url, 
                        payload = "cmd=_notify-validate&" + self.request.body,
                        method=urlfetch.POST,
                        validate_certificate=True
                     )

            logging.info(result.content)

            if result.status_code == 200 and result.content == 'VERIFIED': # OK

                # See pages 19-20 of the Instant Payment Notification Guide at
                # https://cms.paypal.com/cms_content/US/en_US/files/developer/IPNGuide.pdf
                # for various actions that should be taken based on the IPN values.

                ipn_values = cgi.parse_qs(self.request.body)
                debug_msg = '\n'.join(["%s=%s" % (k,'&'.join(v)) for (k,v) in ipn_values.items()])

                # The Sandbox users don't have real mailboxes, so bcc the GAE email sender as a way to
                # debug during development
                self.sendMail(ipn_values['first_name'][0], ipn_values['last_name'][0], ipn_values['payer_email'], 
                              debug_msg=debug_msg, bcc=gae_email_sender)
            else:
                logging.error('Could not fetch %s (%i)' % (url, result.status_code,))

        else:
            logging.error("Unknown mode for POST request!")

def main():
    application = webapp.WSGIApplication([('/', IPNHandler),
                                          ('/(ipn)', IPNHandler)],
                                         debug=True)
    util.run_wsgi_app(application)

if __name__ == '__main__':
    main()

In short, the sample code accepts a POST request, prepends the mandatory cmd=_notify-validate preamble to its body, and sends it back to PayPal. Assuming that PayPal sends back VERIFIED and the response code is 200 (OK), it sends an email to the payer_email that’s provided in the original IPN message and appends the full contents of the IPN so that you can easily view it without having to log in to the GAE console to view the server logs. Because Sandbox accounts don’t have real email addresses, however, it BCCs the gae_email_sender to ensure that the email actually arrives at a real address so that you know it’s working properly. In production, you’d remove the bcc and debug parameters.

Take a moment to study the sample code, and then deploy it to AppSpot after you’ve updated it with a valid email address that is registered with your GAE account. Recall that the IPN handler must be addressable on the Web by PayPal (which cannot be a http://localhost address), so you must actually deploy your GAE project code to AppSpot in order to test your IPN listener.

Note

Whether it’s on GAE or anywhere else, running a mail server during development can often be tricky and frustrating. You can run a local SMTP server that logs out messages it receives to the console using the Python smtpd module by executing the following command in a terminal:

$ python -m smtpd -n -c DebuggingServer localhost:1025

and configuring your local GAE development environment to send mail to it by starting the GAE Python Development Server from the command line with a few options, as follows:

$ /path/to/your/dev_appserver.py --smtp_host=localhost 
--smtp_port=1025 /path/to/your/project

With some sample code in place, let’s use the simulator that’s available from within the PayPal Sandbox under the Test Tools tab to try out our IPN listener, as shown in Figure 6-3. After sending a notification with the simulator, you should see that it acknowledges that an IPN was successfully sent if it is able to successfully reach your handler and no internal server errors occur in your handler. Shortly thereafter, you should also receive an email with the IPN details since that’s what the handler does. (If for some reason you do not receive the email, be sure to check your spam folder before digging into your application’s logs, which are available through the GAE Dashboard.)

Now that you have some code in place that’s capable of handling notifications from the IPN simulator, let’s configure your merchant account for receiving IPNs so that you are able to automatically receive IPNs in response to payment transactions that affect that merchant account. Although it’s possible to specify IPN notification URLs directly in API calls, perhaps the most common way to register for IPNs is to log in to a merchant account and specify a default URL under the merchant account’s “Instant Payments Notification preferences” from within the Profile tab, as shown in Figures 6-4 and 6-5. Note that if you have multiple merchant accounts setup in your Sandbox environment, you’ll need to ensure that you use the 3-token credentials associated with the same merchant account that you used to add the notification URL, and once you set up this IPN notification URL, PayPal will begin sending IPNs to it by default for all IPN-eligible transactions associated with those 3-token credentials.

Log in to your merchant account and select “Instant Payment Notification preferences” under the Profile tab in order to add a Notification URL

Figure 6-4. Log in to your merchant account and select “Instant Payment Notification preferences” under the Profile tab in order to add a Notification URL

PayPal provides the ability to send all IPNs for a merchant account to a notification URL that you specify in your merchant account settings

Figure 6-5. PayPal provides the ability to send all IPNs for a merchant account to a notification URL that you specify in your merchant account settings

Once you’ve deployed your IPN handler and registered a merchant account for receiving IPNs, you should start automatically receiving them whenever a successful transaction occurs that affects that merchant account. A recommended exercise for this chapter involves taking sample project code from a previous chapter and using it to trigger an IPN. Just be advised that once you register for IPNs, you will receive them for all IPN-eligible transactions affecting that account, and PayPal will continue resending them until you acknowledge or disable the IPNs from your merchant account profile.