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.
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.”
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)
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-4. Product.py
# The Product class provides product details. # A more flexible product line could be managed in a database class Product(object): @staticmethod def getProduct(): return {'price' : 9.99, 'quantity' : 30, 'units' : 'days'}
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))
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.