Implementing a Checkout Experience for Tweet Relevance

Let’s take the newfound knowledge from this chapter and use it to implement an Express Checkout for Tweet Relevance, the sample project described in Appendix A that we’ll be using throughout this book. Recall that Tweet Relevance is essentially a mechanism to rank the tweets in a Twitter user’s home timeline by relevance instead of the de facto chronological view that’s typical of most Twitter user interfaces. Since so many Twitter users are plagued by information (tweet) overload, it seems reasonable to think that they would be willing to pay a nominal fee for access to a service that curates tweets that appear in their home timelines. The remainder of this section involves the selection of a payment model and the details involved in integrating an Express Checkout.

The primary detail that we’ll need to settle upon before implementing a payment flow for Tweet Relevance is the payment model. In other words, we must determine what the specific product is that a buyer is purchasing in order to access Tweet Relevance. Although there are perhaps other options that could make sense, a few primary options come to mind:

To get up and running with Express Checkout in this chapter, let’s opt to implement the fixed-rate access payment model for Tweet Relevance.

A minimal integration with Express Checkout is pretty straightforward. Recalling from Appendix A that users must access /app to access the application, this particular API seems to be a reasonable place to inject a Checkout Entry Point to kick off the payment flow. Conceptually, all that needs to happen is a redirect to a special page that displays a “Checkout with PayPal” button and alerts the user that they must pay to access the service. From there, clicking the button kicks off an Express Checkout, which ultimately returns the user to the application once a payment is successfully processed. Having a basic understanding of how Tweet Relevance is designed from reviewing Appendix A and its source code, let’s try to come up with a detailed design for injecting a Checkout Entry Point:

Add User and Product classes

In order to implement a payment model, it’s necessary to create abstractions for user accounts and products. The Product class should feature only a single product that is 30 days of access to the application for $9.99. The User class should be a minimalist account abstraction that keeps enough state for determining whether or not an account is in good standing based on when access was purchased and how much access was purchased.

Add a PaymentHandler module to the handlers package

Tweet Relevance has an AppHandler module that encapsulates core application logic and public APIs, so let’s add a separate PaymentHandler that provides the same kind of encapsulation for payment-related logic and APIs.

Add a paypal package

We could further refine the logic in PaymentHandler by separating the core logic associated with making API requests for Express Checkout from the more general GAE web app logic associated with the PaymentHandler.

Modify AppHandler to limit access based on Users

After users authenticate with their Twitter account, check to see whether or not they have an account in good standing. If they don’t have an account in good standing, redirect them to a Checkout Entry Point so that they can create or reconcile an account to gain access.

Add a template for the Checkout Entry Point into AppHandler

We’ll add a separate template that can serve as a Checkout Entry Point and have AppHandler serve it up as needed to kick off an Express Checkout.

The sample code for Tweet Relevance is essentially stateless. Aside from memcache being used to implement a minimalist session so that the application can serve data to a rich Ajax client, the application does not store any persistent state. The addition of an account abstraction such as User, however, requires persisting state in a reliable and fault-tolerant datastore since customers rightly expect to receive access to the application once they have paid for it. Fortunately, GAE provides just the kind of datastore we need, and the basics of integrating with it are fairly intuitive. We won’t be doing anything very advanced, but it is nonetheless recommended that you bookmark and review The Python Datastore API if you’re not familiar with it.

Examples 2-1 and 2-2 introduce the User and Product modules, which are added to the top level of the project in User.py and Product.py files. Product is just a stub method that returns static product information that could otherwise be managed in a more sophisticated way, but is sufficient for our purposes. User stores the minimal information necessary to limit account access based on the payment model selected: an account identifier, when access began, and how long access should last. Whether or not an account is in good standing can be computed by subtracting the User’s access_start field from the current value returned by datetime.datetime.now() and inspecting the number of days in the resulting datetime.timedelta value.

The substantive updates to AppHandler are shown in Example 2-3. AppHandler directly calls accountIsCurrent as a helper method since different payment models would have different criteria for determining whether or not an account is current. The creditUserAccount method is exposed as a static method so that it may trivially be invoked by PaymentHandler to credit accounts. Arguably, this logic could have remained in PaymentHandler. However, PaymentHandler has no direct dependency or interaction with User, and it seemed prudent to maintain this separation and delegate the responsibility back to the AppHandler, which already has extensive interaction with User.

With an understanding of Product and User in place, let’s now turn to the subject of integrating payment-specific logic via PaymentHandler. Example 2-4 illustrates an updated main.py file that includes a new reference to PaymentHandler that services APIs and encapsulates payment logic. Recall that PaymentHandler itself takes care of the web application logic associated with callbacks for an Express Checkout; internally, it will reference a paypal module that will handle the specific APIs, such as SetExpressCheckout, GetExpressCheckoutDetails, and DoExpressCheckoutPayment.

To keep the project structure nice and tidy, the AppHandler and PaymentHandler classes are maintained in separate files (AppHandler.py and PaymentHandler.py) and reside in a directory called handlers. In Python parlance, we’d say that we have a handlers package that contains AppHandler and PaymentHandler modules. Note that there was no restriction to necessarily separate out the classes into different files, to name the files the same as the classes, or to even maintain them outside of main.py at all. These decisions are simply one possible way to organize and maintain the source code.

As shown in Example 2-4, the logic for encapsulating the Express Checkout-related integration points is encapsulated by the PaymentHandler class, which serves the following URL requests:

/set_ec

A POST request to this URL sets up the transaction by calling SetExpressCheckout with the minimal required parameters to get a session token: the seller’s 3-token credentials, the payment amount, and the return URLs that PayPal should use when redirecting users back to your website depending on whether they cancel or complete the transaction. Assuming that an OK (HTTP 200) response is returned from SetExpressCheckout, a session token is returned in the response, and the application immediately redirects the buyer to PayPal for completion of the purchase. The specific URL that is used for redirection within a sandbox context is https://www.sandbox.paypal.com/webscr?cmd=_express-checkout&token=xxx. If the purchase is completed successfully, PayPal redirects the buyer back to this application’s /get_ec_details or /cancel_ec URLs (as defined by the RETURNURL and CANCELURL values, respectively.) Figure 2-7 displays the jumping entry point that invokes /set_ec and Figures 2-8 and 2-9 display the user interface provided by PayPal as part of an Express Checkout.

Note

The minimal implementation of Express Checkout that’s presented in Example 2-5 doesn’t pass in additional NVP parameters such as L_PAYMENTREQUEST_m_NAMEn to display details associated with an order summary and opts to use GetExpressCheckoutDetails to display a confirmation on your site. In Chapter 3, however, you’ll implement an Express Checkout (for Digital Goods) that displays the order details on PayPal’s site and bypasses the GetExpressCheckoutDetails on your site to accomplish a more streamlined in context payment flow.

/get_ec_details

This URL is accessed as a GET request and provides a confirmation page summarizing the purchase and is accessed by means of PayPal redirecting the buyer back to the application once the purchase has been approved. Although not technically required, it’s a best practice to call GetExpressCheckoutDetails, and Tweet Relevance accesses it and uses the values it returns to illustrate how to provide a confirmation page before completing the payment flow. Figure 2-10 displays a confirmation page generated from information provided by way of /get_ec_details.

/do_ec_payment

This URL is accessed as a GET request when the buyer approves the purchase from the confirmation page provided when /get_ec_details is accessed (which is after PayPal successfully redirects back to it). It finalizes the purchase by executing DoExpressCheckoutPayment to finalize the payment and then interfaces with AppHandler to credit a user’s account with login tokens. Figures 2-11 displays a successful payment confirmation page generated via /do_ec_payment.

/cancel_ec

If a user cancels out of the payment flow while visiting PayPal’s site to approve the purchase, PayPal redirects to this URL by means of a GET request.

If the way that the Express Checkout was injected into the application is not fairly clear by this point, it may be helpful to reference Figure 2-6 and explicitly map the general payment flow to the APIs exposed by PaymentHandler. Of course, running the application’s updated sample code for this chapter should be the simplest course of action to take at this point if you haven’t done so already.

Finally, Examples 2-5 and 2-6 introduce the payment-related details associated with the sample code for this chapter. As is the case with the templates referenced in AppHandler, the templates referenced in PaymentHandler are nothing more than very minimal HTML pages that plug in named variables so that the view and controller of the application can be separated. Although there’s a lot of code in these listings, it’s not very complex. Example 2-5 is routing the various URL requests associated with the PaymentHandler’s Express Checkout operations through to PayPal, checking response values, and taking appropriate actions. Example 2-6 is a minimal class definition to create an abstraction for interacting with the Express Checkout product’s API operations so that the PaymentHandler code is tidy and isn’t littered with setting and transacting urlfetch.fetch operations.

Tip

Use a command-line tool such as diff or a text editor that’s capable of producing a side-by-side diff of files to narrow in the exact changes that were made to the baseline Tweet Relevance application in order to implement Express Checkout.

Example 2-5. Tweet-Relevance/handlers/PaymentHandler.py

import os

from google.appengine.ext import webapp
from google.appengine.api import memcache
from google.appengine.ext.webapp import template
import logging
import cgi

from paypal.products import ExpressCheckout as EC
from Product import Product
from handlers.AppHandler import AppHandler

class PaymentHandler(webapp.RequestHandler):

  def post(self, mode=""):

    if mode == "set_ec":

      sid = self.request.get("sid")
      user_info = memcache.get(sid)

      product = Product.getProduct()

      nvp_params = {
              'PAYMENTREQUEST_0_AMT' : str(product['price']),
              'RETURNURL' : self.request.host_url+"/get_ec_details?sid="+sid,
              'CANCELURL': self.request.host_url+"/cancel_ec?sid="+sid
            }

      response = EC.set_express_checkout(nvp_params)

      if response.status_code != 200:
        logging.error("Failure for SetExpressCheckout")

        template_values = {
          'title' : 'Error',
          'operation' : 'SetExpressCheckout'
        }
        
        path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'unknown_error.html')
        return self.response.out.write(template.render(path, template_values))

      # Redirect to PayPal and allow user to confirm payment details.
      # Then PayPal redirects back to the /get_ec_details or /cancel_ec endpoints.
      # Assuming /get_ec_details, we complete the transaction with PayPal.get_express_checkout_details
      # and PayPal.do_express_checkout_payment

      parsed_qs = cgi.parse_qs(response.content)

      redirect_url = EC.generate_express_checkout_redirect_url(parsed_qs['TOKEN'][0])
      return self.redirect(redirect_url)

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

  def get(self, mode=""):

    if mode == "get_ec_details":
      response = EC.get_express_checkout_details(self.request.get("token"))

      if response.status_code != 200:
        logging.error("Failure for GetExpressCheckoutDetails")

        template_values = {
          'title' : 'Error',
          'operation' : 'GetExpressCheckoutDetails'
        }
        
        path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'unknown_error.html')
        return self.response.out.write(template.render(path, template_values))

      product = Product.getProduct()

      parsed_qs = cgi.parse_qs(response.content)

      template_values = {
        'title' : 'Confirm Purchase',
        'quantity' : product['quantity'], 
        'units' : product['units'], 
        'email' : parsed_qs['EMAIL'][0], 
        'amount' : parsed_qs['PAYMENTREQUEST_0_AMT'][0],
        'query_string_params' : self.request.query_string
      }

      path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'confirm_purchase.html')
      self.response.out.write(template.render(path, template_values))

    elif mode == "do_ec_payment":

      if memcache.get(self.request.get("sid")) is not None: # Without an account reference, we can't credit the purchase
        payerid = self.request.get("PayerID")

        product = Product.getProduct()

        nvp_params = { 
                'PAYERID' : payerid, 
                'PAYMENTREQUEST_0_AMT' : str(product['price'])
        }

        response = EC.do_express_checkout_payment(
                        self.request.get("token"), 
                        nvp_params
                   )

        if response.status_code != 200:
          logging.error("Failure for DoExpressCheckoutPayment")

          template_values = {
            'title' : 'Error',
            'operation' : 'DoExpressCheckoutPayment'
          }
        
          path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'unknown_error.html')
          return self.response.out.write(template.render(path, template_values))

      
        # Ensure that the payment was successful
  
        parsed_qs = cgi.parse_qs(response.content)
  
        if parsed_qs['ACK'][0] != 'Success':
          logging.error("Unsuccessful DoExpressCheckoutPayment")
  
          template_values = { 
            'title' : 'Error',
            'details' : parsed_qs['L_LONGMESSAGE0'][0]
          }   
                
          path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'unsuccessful_payment.html')
          return self.response.out.write(template.render(path, template_values))

        if parsed_qs['PAYMENTINFO_0_PAYMENTSTATUS'][0] != 'Completed': # Probably an eCheck
          logging.error("Unsuccessful DoExpressCheckoutPayment")
          logging.error(parsed_qs)
  
          template_values = { 
            'title' : 'Error',
            'details' : 'Sorry, eChecks are not accepted. Please send an instant payment.'
          }   
    
          path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'unsuccessful_payment.html')
          return self.response.out.write(template.render(path, template_values))


        # Credit the user's account

        user_info = memcache.get(self.request.get("sid"))
        twitter_username = user_info['username']
        product = Product.getProduct()

        AppHandler.creditUserAccount(twitter_username, product['quantity'])

        template_values = {
          'title' : 'Successful Payment',
          'quantity' : product['quantity'],
          'units' : product['units']
        }
        
        path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'successful_payment.html')
        self.response.out.write(template.render(path, template_values))

      else:
        logging.error("Invalid/expired session in /do_ec_payment")

        template_values = {
          'title' : 'Session Expired',
        }

        path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'session_expired.html')
        self.response.out.write(template.render(path, template_values))

    elif mode == "cancel_ec":
      template_values = {
        'title' : 'Cancel Purchase',
      }

      path = os.path.join(os.path.dirname(__file__), '..', 'templates', 'cancel_purchase.html')
      self.response.out.write(template.render(path, template_values))