Integrating a “Simple” Adaptive Payment into Tweet Relevance

If you’ve followed along thus far, integrating Adaptive Payments into Tweet Relevance should seem like a fairly melodramatic exercise. The goal of the integration is the same as that of previous chapters: to implement a payment mechanism so that users of the service can be charged for using it. In the interest of getting up and running, let’s integrate a Simple Adaptive Payment in order to implement a basic subscription model in which a customer purchases 30 days of access for a nominal fee. The previous GAE examples have worked through most of the nuts and bolts as related to the Adaptive Payments portion of the exercise, so there’s actually just a very little bit of software engineering involved to perform the integration and smooth out a few rough edges. The remainder of this section assumes that you have familiarity with the baseline Tweet Relevance project code from Appendix A and an appreciation for some of the payment models as described in Implementing a Checkout Experience for Tweet Relevance. Changes to the baseline project structure in order to implement a subscription payment model are addressed on a file-by-file basis.

Note

It may be helpful to review Implementing a Checkout Experience for Tweet Relevance and refresh your memory on the various payment mechanisms that could be viable for a service like Tweet Relevance. The remainder of this chapter assumes familiarity with the options as presented in that section and implements the “subscription model.”

main.py

The overall architecture for the finished web application involving Adaptive Payments mimics the same operations for ExpressCheckout, but we’ll name them a little differently so as not to confuse the two products. Thus, the PaymentHander exposes /pay, /completed_payment, and /cancelled_payment operations that will be mapped by the main application. Thus, main() looks like this:

def main():
    
  application = webapp.WSGIApplication([

                                        # PaymentHandler URLs

                                        ('/(pay)', PaymentHandler),
                                        ('/(completed_payment)', PaymentHandler),    
                                        ('/(cancelled_payment)', PaymentHandler),    

                                        # AppHandler URLs

                                        ('/(app)', AppHandler),
                                        ('/(data)', AppHandler),
                                        ('/(login)', AppHandler),
                                        ('/', AppHandler)
                                       ],  

                                       debug=True)
  util.run_wsgi_app(application)
handlers/PaymentHandler.py

Most of the action for the integration happens in PaymentHandler, which interacts with PayPal and interfaces with the AppHandler to update to credit the account with 30 days of access after a successful payment. The PaymentHandler class in Example 4-5 illustrates how to make it happen. The import and reference for the trivial Product class is shown in Example 4-4. The basic control flow is essentially the same as that involving an Express Checkout: a transaction is set up with /pay, Tweet Relevance redirects to PayPal to approve the transaction, and PayPal redirects back to /completed_payment once the user has approved the payment. The application then confirms with PayPal that the transaction has indeed been completed before handing back control to the ApplicationHandler so as to avoid a fundamental security flaw in which a malicious attacker may be able to gain account credits without actually approving a payment. The same memcache mechanism for associating a payKey value with a user’s session identifier after PayPal redirects back to the application, as described in Example 4-3, is also employed in PaymentHandler.

Finally, although it’s not displayed as example code below, note that the /pay URL is triggered by templates/checkout.html in the project code—the same kind of page shown in Figure 2-7 that displays a yellow “Checkout with PayPal” button.

Example 4-5. handlers/PaymentHandler.py

import os

from google.appengine.ext import webapp
from google.appengine.api import memcache
from google.appengine.ext.webapp import template
from django.utils import simplejson as json
import logging

from paypal.products import AdaptivePayment as AP
from paypal.paypal_config import seller_email as SELLER_EMAIL
from Product import Product
from handlers.AppHandler import AppHandler

class PaymentHandler(webapp.RequestHandler):

  def post(self, mode=""):

    if mode == "pay":

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

      returnUrl = self.request.host_url+"/completed_payment?sid="+sid,
      cancelUrl = self.request.host_url+"/cancelled_payment?sid="+sid

      product = Product.getProduct()

      seller = {'email' : SELLER_EMAIL, 'amount' : product['price']}

      response = AP.pay(receiver=[seller], cancelUrl=cancelUrl, returnUrl=returnUrl)
      result = json.loads(response.content)
      logging.info(result)

      if result['responseEnvelope']['ack'] == 'Failure':
        logging.error("Failure for Pay")

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

      # Stash away the payKey for later use 

      user_info = memcache.get(sid)
      user_info['payKey'] = result['payKey']
      memcache.set(sid, user_info, time=60*10) # seconds

      # Redirect to PayPal and allow user to confirm payment details.

      redirect_url = AP.generate_adaptive_payment_redirect_url(result['payKey'])
      return self.redirect(redirect_url)

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

  def get(self, mode=""):

    if mode == "completed_payment":

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

        payKey = user_info["payKey"]

        response = AP.get_payment_details(payKey)
        result = json.loads(response.content)
        logging.info(result)

        if result['responseEnvelope']['ack'] == 'Failure' or \
           result['status'] != 'COMPLETED': # Something went wrong!

          logging.error("Failure for PaymentDetails")

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


        if result['paymentInfoList']['paymentInfo'][0]['transactionStatus'] != 'COMPLETED': # An eCheck?

          logging.error("Payment transaction status is not complete!")

          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

        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 /completed_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 == "cancelled_payment":
      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))
paypal/products.py

The addition of an AdaptivePayment class (imported as AP to save some typing in PaymentHandler) to the paypal.products module, along with a few minor additions to the paypal.paypal_config to encapsulate configuration information such as the required Adaptive Payments headers and 3-Token credentials, are about all that it takes to round out the remainder of the substantive changes to Tweet Relevance. The AdaptivePayment class follows and is little more than a wrapper around a Pay and PaymentDetails request:

from google.appengine.api import urlfetch
from django.utils import simplejson as json

import urllib
import cgi

import paypal_config

class AdaptivePayment(object):

    @staticmethod
    def _api_call(url, params):

        response = urlfetch.fetch(
                    url,
                    payload=json.dumps(params),
                    method=urlfetch.POST,
                    validate_certificate=True,
                    deadline=10, # seconds
                    headers=paypal_config.adaptive_headers
                   )   

        if response.status_code != 200:
            result = json.loads(response.content)
            logging.error(json.dumps(response.content, indent=2))

            raise Exception(str(response.status_code))

        return response


    # Lists out some of the most common parameters as keyword args. Other keyword args can be added through kw as needed
    # Template for an item in the receiver list: {'email' : me@example.com, 'amount' : 1.00, 'primary' : False}

    @staticmethod
    def pay(sender=None, receiver=[], feesPayer='EACHRECEIVER', memo='', cancelUrl='', returnUrl='', **kw ):

        params = {
            'requestEnvelope' : {'errorLanguage' : 'en_US', 'detailLevel' : 'ReturnAll'},
            'actionType' : 'PAY',
            'currencyCode' : 'USD',
            'senderEmail' : sender,
            'receiverList' : {
                    'receiver' : receiver
            },
            'feesPayer' : feesPayer,
            'memo' : memo,
            'cancelUrl' : cancelUrl,
            'returnUrl' : returnUrl 
        }

        if sender is None: params.pop('senderEmail')

        if memo == "": params.pop('memo')

        params.update(kw)

        return AdaptivePayment._api_call(paypal_config.adaptive_sandbox_api_pay_url, params)

    @staticmethod
    def get_payment_details(payKey):

        params = {
            'requestEnvelope' : {'errorLanguage' : 'en_US', 'detailLevel' : 'ReturnAll'},
            'payKey' : payKey
        }

        return AdaptivePayment._api_call(paypal_config.adaptive_sandbox_api_payment_details_url, params)

    @staticmethod
    def generate_adaptive_payment_redirect_url(payKey, embedded=False):
        if embedded:
            return "https://www.sandbox.paypal.com/webapps/adaptivepayment/flow/pay?payKey=%s" % (payKey,)
        else:
            return "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_ap-payment&paykey=%s" % (payKey,)

If the Adaptive Payments integration details into Tweet Relevance really do seem melodramatic, it’s an indicator that your learning is well on track and that you should have little trouble using Adaptive Payments for your own application. If you haven’t already, however, please take a moment to peruse the final project code as a final exercise.