Skip to content

Commit

Permalink
Expose and use the new Pledge handler.
Browse files Browse the repository at this point in the history
Adds tests, and production versions of the factored-out
dependencies. Changes pledge.jade to use the new handler.
  • Loading branch information
hjfreyer committed May 26, 2014
1 parent a48e69b commit 34934a9
Show file tree
Hide file tree
Showing 7 changed files with 443 additions and 148 deletions.
119 changes: 106 additions & 13 deletions backend/handlers.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,101 @@
"""Handlers for MayOne.US."""

from collections import namedtuple
import datetime
import json
import logging
import webapp2

from google.appengine.ext import deferred
import validictory
import webapp2

import model

# Immutable environment with both configuration variables, and backends to be
# mocked out in tests.
Environment = namedtuple(
'Environment',
[
# AppEngine app name.
# App engine app name, or 'local' for dev_appserver, or 'unittest' for unit
# tests.
'app_name',

# Stripe creds to export.
'stripe_public_key',

# PaymentProcessor
'payment_processor',

# MailingListSubscriber
'mailing_list_subscriber',

# MailSender
'mail_sender',
])


class PaymentProcessor(object):
"""Interface which processes payments."""
def CreateCustomer(self, payment_params, pledge_model):

STRIPE = 'STRIPE'

This comment has been minimized.

Copy link
@jtolio

jtolio May 27, 2014

Contributor

this seems a little weird to me - cross cutting concerns. the base payment processor abstract class needs to worry about the form representation of the stripe subclass? but yeah okay

This comment has been minimized.

Copy link
@hjfreyer

hjfreyer May 27, 2014

Author Contributor

Totally agree. I'll change this interface to be stripe-specific. We can figure out what's up with Paypal later.


def CreateCustomer(self, params):
"""Does whatever the payment processor needs to do in order to be able to
charge the customer later.
Args:
payment_params: dict with keys like 'paypal' or 'stripe', with values
which are dicts with parameters specific to that payment platform.
pledge_model: A not-yet-committed pledge model for us to modify to include
a record of the customer.
params: dict representing the JSON object send to the pledge creation
handler.
Returns: A dict giving the fields that will need to be included in the
Pledge model.
"""
raise NotImplementedError()


class MailingListSubscriber(object):
"""Interface which signs folks up for emails."""
def Subscribe(self, first_name, last_name, amount_cents, ip_addr, time,
def Subscribe(self, email, first_name, last_name, amount_cents, ip_addr, time,
source):
raise NotImplementedError()


class MailSender(object):
"""Interface which sends mail."""
def Send(self, to, subject, text_body, html_body):
raise NotImplementedError()


_STR = dict(type='string')
class PledgeHandler(webapp2.RequestHandler):
"""RESTful handler for pledge objects."""

CREATE_SCHEMA = dict(
type='object',
properties=dict(
email=_STR,
phone=dict(type='string', blank=True),
firstName=_STR,
lastName=_STR,
name=_STR,
occupation=_STR,
employer=_STR,
target=_STR,
subscribe=dict(type='boolean'),
amountCents=dict(type='integer', minimum=100)
amountCents=dict(type='integer', minimum=100),
payment=dict(type='object',
properties=dict(
STRIPE=dict(type='object',
required=False,
properties=dict(token=_STR)),
# TODO: Paypal
)
),
)
)

def post(self):
"""Create a new pledge, and update user info."""
env = self.app.config['env']

try:
data = json.loads(self.request.body)
except ValueError, e:
Expand All @@ -82,10 +112,73 @@ def post(self):
self.response.write('Invalid request')
return

# Do any server-side processing the payment processor needs.
stripe_customer_id = None
if PaymentProcessor.STRIPE in data['payment']:
customer = env.payment_processor.CreateCustomer(data)
stripe_customer_id = customer['customer_id']
else:
logging.warning('No payment processor specified: %s', data)
pledge = model.addPledge(email=data['email'],
stripe_customer_id=stripe_customer_id,
amount_cents=data['amountCents'],
occupation=data['occupation'],
employer=data['employer'],
phone=data['phone'],
fundraisingRound='1',
target=data['target'])

# Split apart the name into first and last. Yes, this sucks, but adding the
# name fields makes the form look way more daunting. We may reconsider this.

This comment has been minimized.

Copy link
@jtolio

jtolio May 27, 2014

Contributor

doh! yeah, whoa :(

name_parts = data['name'].split(None, 1)

This comment has been minimized.

Copy link
@jtolio

jtolio May 27, 2014

Contributor

so the last name is all but the first word?

This comment has been minimized.

Copy link
@hjfreyer

hjfreyer May 27, 2014

Author Contributor

Yes. =\

first_name = name_parts[0]
if len(name_parts) == 1:
last_name = ''
logging.warning('Could not determine last name: %s', data['name'])
else:
last_name = name_parts[1]

if data['subscribe']:

This comment has been minimized.

Copy link
@jtolio

jtolio May 27, 2014

Contributor

silly, but maybe we want to check and parse this? like, if data["subscribe"] == "false" or something, maybe we don't want to do this, even though bool("false") == True

This comment has been minimized.

Copy link
@jtolio

jtolio May 27, 2014

Contributor

like most of my comments, feel free to entirely ignore. just some thinking out loud. subscribe not being sent if false is also a perfectly fine contract

This comment has been minimized.

Copy link
@hjfreyer

hjfreyer May 27, 2014

Author Contributor

data['subscribe'] has to already be a boolean, or validictory will complain.

env.mailing_list_subscriber.Subscribe(
email=data['email'],
first_name=first_name, last_name=last_name,
amount_cents=data['amountCents'],
ip_addr=self.request.remote_addr,
time=datetime.datetime.now(),
source='pledged')

format_kwargs = {
'name': data['name'].encode('utf-8'),
'url_nonce': pledge.url_nonce,
'total': '$%d' % int(data['amountCents'] / 100)
}

text_body = open('email/thank-you.txt').read().format(**format_kwargs)
html_body = open('email/thank-you.html').read().format(**format_kwargs)

env.mail_sender.Send(to=data['email'].encode('utf-8'),
subject='Thank you for your pledge',
text_body=text_body,
html_body=html_body)

self.response.headers['Content-Type'] = 'application/json'
json.dump(dict(id=str(pledge.key()),
auth_token=pledge.url_nonce), self.response)


class PaymentConfigHandler(webapp2.RequestHandler):
def get(self):
env = self.app.config['env']
if not env.stripe_public_key:
raise Error('No stripe public key in DB')
params = dict(testMode=(env.app_name == u'local'),
stripePublicKey=env.stripe_public_key)

self.response.headers['Content-Type'] = 'application/json'
json.dump(dict(id='2'), self.response)
json.dump(params, self.response)


HANDLERS = [
('/r/pledge', PledgeHandler),
('/r/payment_config', PaymentConfigHandler),
]
93 changes: 72 additions & 21 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import datetime
import jinja2
import json
import logging
import urlparse
import webapp2

from google.appengine.api import mail
from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.ext import deferred

from mailchimp import mailchimp
import jinja2
import stripe
import webapp2

import handlers
import model
import stripe
import wp_import

# These get added to every pledge calculation
Expand All @@ -32,25 +31,14 @@ class Error(Exception): pass
autoescape=True)


def send_thank_you(name, email, url_nonce, amount_cents):
def send_mail(to, subject, text_body, html_body):
"""Deferred email task"""
sender = ('MayOne no-reply <noreply@%s.appspotmail.com>' %
model.Config.get().app_name)
subject = 'Thank you for your pledge'
message = mail.EmailMessage(sender=sender, subject=subject)
message.to = email

format_kwargs = {
# TODO: Figure out how to set the outgoing email content encoding.
# once we can set the email content encoding to utf8, we can change this
# to name.encode('utf-8') and not drop fancy characters. :(
'name': name.encode('ascii', errors='ignore'),
'url_nonce': url_nonce,
'total': '$%d' % int(amount_cents/100)
}

message.body = open('email/thank-you.txt').read().format(**format_kwargs)
message.html = open('email/thank-you.html').read().format(**format_kwargs)
message.to = to
message.body = text_body
message.html = html_body
message.send()


Expand Down Expand Up @@ -133,7 +121,8 @@ def get(self):
self.response.headers['Content-Type'] = 'application/javascript'
self.response.write('%s(%d)' % (self.request.get('callback'), total))


# DEPRECATED. Replaced by handlers.PaymentConfigHandler
# TODO(hjfreyer): Remove
class GetStripePublicKeyHandler(webapp2.RequestHandler):
def get(self):
if not model.Config.get().stripe_public_key:
Expand All @@ -149,6 +138,8 @@ def get(self):
self.redirect('/')


# DEPRECATED. Replaced by handlers.PledgeHandler
# TODO(hjfreyer): Remove
class PledgeHandler(webapp2.RequestHandler):
def post(self):
try:
Expand Down Expand Up @@ -273,6 +264,66 @@ def post(self, url_nonce):
return


class ProdPaymentProcessor(handlers.PaymentProcessor):
def __init__(self, stripe_private_key):
self.stripe_private_key = stripe_private_key

def CreateCustomer(self, request):
if 'STRIPE' not in request['payment']:
raise Error('STRIPE is the only supported payment platform')
stripe.api_key = self.stripe_private_key
customer = stripe.Customer.create(
card=request['payment']['STRIPE']['token'],
email=request['email'])
return dict(customer_id=customer.id)


class FakePaymentProcessor(handlers.PaymentProcessor):
def CreateCustomer(self, request):
logging.error('USING FAKE PAYMENT PROCESSOR')
return dict(customer_id='fake_1234')


class MailchimpSubscriber(handlers.MailingListSubscriber):
def Subscribe(self, email, first_name, last_name, amount_cents, ip_addr, time,
source):
deferred.defer(subscribe_to_mailchimp,
email, first_name, last_name,
amount_cents, ip_addr, 'pledge')


class FakeSubscriber(handlers.MailingListSubscriber):
def Subscribe(self, **kwargs):
logging.info('Subscribing %s', kwargs)


class ProdMailSender(handlers.MailSender):
def Send(self, to, subject, text_body, html_body):
deferred.defer(send_mail, to, subject, text_body, html_body)


def GetEnv():
j = json.load(open('config.json'))
s = model.Secrets.get()

payment_processor = None
mailing_list_subscriber = None
if j['appName'] == 'local':
payment_processor = FakePaymentProcessor()
mailing_list_subscriber = FakeSubscriber()
else:
payment_processor = ProdPaymentProcessor(
model.Config.get().stripe_private_key)
mailing_list_subscriber = MailchimpSubscriber()

return handlers.Environment(
app_name=j['appName'],
stripe_public_key=model.Config.get().stripe_public_key,
payment_processor=payment_processor,
mailing_list_subscriber=mailing_list_subscriber,
mail_sender=ProdMailSender())


app = webapp2.WSGIApplication([
('/total', GetTotalHandler),
('/stripe_public_key', GetStripePublicKeyHandler),
Expand All @@ -282,4 +333,4 @@ def post(self, url_nonce):
('/contact.do', ContactHandler),
# See wp_import
# ('/import.do', wp_import.ImportHandler),
] + handlers.HANDLERS, debug=False)
] + handlers.HANDLERS, debug=False, config=dict(env=GetEnv()))
2 changes: 2 additions & 0 deletions backend/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class Error(Exception): pass
#
# Note that this isn't really a "model", it's built up from config.json
# and the "Secrets" model.
#
# TODO(hjfreyer): Deprecate this and replace it with handlers.Environment.
class Config(object):
ConfigType = namedtuple('ConfigType',
['app_name',
Expand Down
Loading

0 comments on commit 34934a9

Please sign in to comment.