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:
In fixed-rate access (pay per access) model, a user might pay a small fee in order to access the service for a specified duration. For example, a special “trial access” product to the service might cost $0.99 and provide unlimited access for 24 hours, unlimited access for 30 days might cost $9.99, and unlimited access for 365 days access might cost $99.99.
A subscription service might provide unlimited access to the application for a longer-term duration such as a month or year. For example, a subscription might cost $8.99 for 30 days of unlimited access or $89.99 for 365 days of unlimited access. It is fairly common that subscriptions automatically renew once they expire and sometimes offer a discount in comparison to a fixed-rate access model as an incentive.
A virtual currency such as “login tokens” might allow users to log in and access the application for a limited duration. For example, a bundle of 50 tokens might cost $4.99, and users would expend a single token each time that they login to access the application.
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:
User
and Product
classesIn 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.
PaymentHandler
module to the
handlers
packageTweet 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.
paypal
packageWe 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
.
AppHandler
to limit access based on
User
sAfter 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.
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
Payment
Handler
. 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
.
Example 2-1. Tweet Relevance—Product.py
class Product(object): @staticmethod def getProduct(): return {'price' : 9.99, 'quantity' : 30, 'units' : 'days'}
Example 2-2. Tweet Relevance—User.py
from google.appengine.ext import db class User(db.Model): twitter_username = db.StringProperty(required=True) # Set to "now" the first time the instance is added to the datastore access_start = db.DateTimeProperty(required=True, auto_now_add=True) # Days access_duration = db.IntegerProperty(required=True, default=30)
Example 2-3. Tweet Relevance—methods added to AppHandler.py
@staticmethod def creditUserAccount(twitter_username, num_days): query = User.all().filter("twitter_username =", twitter_username) user = query.get() user.access_start = datetime.datetime.now() user.access_duration = num_days db.put(user) @staticmethod def accountIsCurrent(user): days_used = (datetime.datetime.now() - user.access_start).days + 1 return days_used < user.access_duration
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
.
Example 2-4. Tweet Relevance—main.py
# Minimal GAE imports to run the app from google.appengine.ext import webapp from google.appengine.ext.webapp import util # Logic for implementing Express Checkout from handlers.PaymentHandler import PaymentHandler # Logic for the app itself from handlers.AppHandler import AppHandler # Logic for interacting with Twitter's API and serving up data, etc. def main(): application = webapp.WSGIApplication([ # PaymentHandler URLs ('/(set_ec)', PaymentHandler), ('/(get_ec_details)', PaymentHandler), ('/(do_ec_payment)', PaymentHandler), ('/(cancel_ec)', PaymentHandler), # AppHandler URLs ('/(app)', AppHandler), ('/(data)', AppHandler), ('/(login)', AppHandler), ('/', AppHandler) ], debug=True) util.run_wsgi_app(application) if __name__ == '__main__': main()
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:
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.
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_NAME
n
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.
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
.
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
.
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.
Figure 2-7. Tweet Relevance implements the Checkout Entry Point by presenting an opportunity for the user to purchase login requests. Clicking the “Checkout with PayPal” button invokes SetExpressCheckout and initiates the checkout process. (See Steps 1 and 2 of Figure 2-6.)
Figure 2-8. After calling SetExpressCheckout, Tweet Relevance redirects the buyer to PayPal for payment approval. PayPal redirects buyers back to Tweet Relevance once they’ve authorized Tweet Relevance to charge them. (See Steps 3 and 4 of Figure 2-6.)
Figure 2-9. Express Checkout features a streamlined interface that's optimized for mobile devices and "just works" without any additional action required by developers.
Figure 2-10. Use GetExpressDetails to present the user with a final confirmation before calling DoExpressCheckoutPayment and finalizing the transaction. (See Step 5 of Figure 2-6.)
Figure 2-11. Invoking DoExpressCheckoutPayment finalizes the payment and presents the user with an opportunity to log in and use the application. (See Step 6 of Figure 2-6.)
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.
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))
Example 2-6. Tweet-Relevance/paypal/products.py
from google.appengine.api import urlfetch import urllib import cgi import paypal_config def _api_call(nvp_params): params = nvp_params.copy() # copy to avoid mutating nvp_params with update() params.update(paypal_config.nvp_params) # update with 3 token credentials and api version response = urlfetch.fetch( paypal_config.sandbox_api_url, payload=urllib.urlencode(params), method=urlfetch.POST, validate_certificate=True, deadline=10 # seconds ) if response.status_code != 200: decoded_url = cgi.parse_qs(result.content) for (k,v) in decoded_url.items(): logging.error('%s=%s' % (k,v[0],)) raise Exception(str(response.status_code)) return response class ExpressCheckout(object): @staticmethod def set_express_checkout(nvp_params): nvp_params.update(METHOD='SetExpressCheckout') return _api_call(nvp_params) @staticmethod def get_express_checkout_details(token): nvp_params = {'METHOD' : 'GetExpressCheckoutDetails', 'TOKEN' : token} return _api_call(nvp_params) @staticmethod def do_express_checkout_payment(token, nvp_params): nvp_params.update(METHOD='DoExpressCheckoutPayment', TOKEN=token) return _api_call(nvp_params) @staticmethod def generate_express_checkout_redirect_url(token): return "https://www.sandbox.paypal.com/webscr?cmd=_express-checkout&token=%s" % (token,)