Last active
December 17, 2015 09:39
-
-
Save tav/5589012 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import logging | |
from cgi import escape | |
from decimal import Decimal | |
from json import loads as decode_json | |
from google.appengine.api import memcache | |
from google.appengine.api.urlfetch import fetch as urlfetch, POST | |
from google.appengine.ext import db | |
create_key = db.Key.from_path | |
CAMPAIGNS = [ | |
('sketchup', 500000, 'SketchUp Extension', 'Blah blah blah'), | |
('hardware', 25000000, 'Hardware Designs', 'More blah blah'), | |
('platform', 18000000, 'Platform Development', 'Even more blah'), | |
] | |
CAMPAIGN_KEYS = {} | |
for _campaign in CAMPAIGNS: | |
CAMPAIGN_KEYS[_campaign[0]] = create_key('C', _campaign[0]) | |
del _campaign | |
CURRENCIES = frozenset(['GBP', 'EUR', 'USD']) | |
PAYPAL_ACCOUNT = '[email protected]' | |
class C(db.Model): # key=campaign_id | |
v = db.IntegerProperty(default=0) | |
c = db.IntegerProperty(default=0) # count of funders | |
t1 = db.IntegerProperty(default=0) # total raised (GBP) | |
t2 = db.IntegerProperty(default=0) # total raised (EUR) | |
t3 = db.IntegerProperty(default=0) # total raised (USD) | |
Campaign = C | |
class P(db.Model): # key=txn_id | |
v = db.IntegerProperty(default=0) | |
c = db.StringProperty(default='', indexed=False) # campaign id | |
d = db.DateTimeProperty(auto_now_add=True) # date created | |
e = db.StringProperty(default='', indexed=False) # payer email | |
f = db.StringProperty(default='', indexed=False) # fee | |
g = db.StringProperty(default='', indexed=False) # gross | |
h = db.BooleanProperty(default=False) # handled | |
i = db.TextProperty() # info payload | |
m = db.DateTimeProperty(auto_now=True) # modified date | |
n = db.StringProperty(default='', indexed=False) # net | |
p = db.StringProperty(default='', indexed=False) # payer name | |
t = db.StringProperty(default='', indexed=False) # currency type | |
PayPalTransaction = P | |
class T(db.Model): # parent=campaign_id, key=txn_id | |
"""Stub entity to synchronise accounted transactions.""" | |
TransactionReceipt = T | |
# This handler needs to be served at the path: /ipn | |
# | |
# The required parameters are: | |
# | |
# post_body — The body of the POST request that PayPal made to /ipn | |
# This would have been encoded in windows-1252 and we | |
# shouldn't corrupt it by trying to treat it as utf-8 | |
# | |
# kwargs — The key/value pairs from the parsed POST body. | |
# | |
# The returned string value from this handler needs to be served with a | |
# 200 OK code. | |
def handle_ipn(post_body, **kwargs): | |
resp = do_actual_ipn_handling(post_body, kwargs) | |
if resp != "OK": | |
logging.info("IPN RETCODE: %s" % resp) | |
return resp | |
def do_actual_ipn_handling(txn_info, kwargs): | |
if kwargs['payment_status'] != 'Completed': | |
return '1' | |
resp = urlfetch( | |
url="https://www.paypal.com/cgi-bin/webscr", | |
method=POST, | |
payload=('cmd=_notify-validate&' + txn_info) | |
) | |
if resp.content != 'VERIFIED': | |
return '2' | |
campaign = kwargs['custom'] | |
if not campaign: | |
return '3' | |
if not campaign.startswith('wikihouse.'): | |
return '4' | |
campaign = campaign.split('.', 1)[1].strip() | |
if campaign not in CAMPAIGN_KEYS: | |
return '5' | |
receiver = kwargs['receiver_email'] | |
if receiver != PAYPAL_ACCOUNT: | |
return '6' | |
currency = kwargs['mc_currency'] | |
if currency not in CURRENCIES: | |
return '7' | |
gross = kwargs['mc_gross'] | |
fee = kwargs['mc_fee'] | |
gross_d = Decimal(gross) | |
fee_d = Decimal(fee) | |
net = gross_d - fee_d | |
if net <= 0: | |
return '8' | |
txn_id = kwargs['txn_id'] | |
first_name = kwargs.get('first_name') | |
if first_name: | |
first_name += ' ' | |
payer_name = (first_name + kwargs.get('last_name', '')).strip() | |
payer_email = kwargs['payer_email'] | |
txn = PayPalTransaction.get_or_insert( | |
key_name=txn_id, c=campaign, e=payer_email, f=fee, g=gross, i=txn_info, | |
n=str(net), p=payer_name, t=currency | |
) | |
db.run_in_transaction( | |
update_campaign_tallies, campaign, txn_id, currency, net | |
) | |
txn.h = True | |
txn.put() | |
return 'OK' | |
def update_campaign_tallies(campaign_id, txn_id, currency, amount): | |
amount_in_pence = int(amount * 100) | |
campaign_key = CAMPAIGN_KEYS[campaign_id] | |
receipt = db.get(create_key('T', txn_id, parent=campaign_key)) | |
if receipt: | |
return | |
receipt = TransactionReceipt(key_name=txn_id, parent=campaign_key) | |
campaign = Campaign.get(campaign_key) | |
campaign.c += 1 | |
if currency == 'GBP': | |
campaign.t1 += amount_in_pence | |
elif currency == 'EUR': | |
campaign.t2 += amount_in_pence | |
elif currency == 'USD': | |
campaign.t3 += amount_in_pence | |
db.put([campaign, receipt]) | |
return | |
def render_number_with_commas(n): | |
result = '' | |
while n >= 1000: | |
n, r = divmod(n, 1000) | |
result = ",%03d%s" % (r, result) | |
return "%d%s" % (n, result) | |
def get_exchange_rate_to_gbp(currency, cache={}): | |
if currency == 'GBP': | |
return 1 | |
if currency in cache: | |
return cache[currency] | |
rate = memcache.get('exchange:%s' % currency) | |
if rate: | |
return cache.setdefault(currency, rate) | |
url = "http://rate-exchange.appspot.com/currency?from=%s&to=GBP" % currency | |
try: | |
rate = decode_json(urlfetch(url).content)['rate'] | |
except Exception, err: | |
logging.error("currency conversion: %s" % err) | |
return 0 | |
memcache.set('exchange:%s' % currency, rate) | |
return cache.setdefault(currency, rate) | |
def gen_fund_form(): | |
html = []; out = html.append | |
entities = dict( | |
(ent.key().name(), ent) | |
for ent in Campaign.get(sorted(CAMPAIGN_KEYS)) | |
) | |
for campaign in CAMPAIGNS: | |
campaign_id = campaign[0] | |
ent = entities[campaign_id] | |
if ent.c == 1: | |
backers = '1 Backer' | |
else: | |
backers = '%d Backers' % ent.c | |
target = campaign[1] | |
total = ent.t1 | |
if ent.t2: | |
total += (ent.t2 * get_exchange_rate_to_gbp('EUR')) | |
if ent.t3: | |
total += (ent.t3 * get_exchange_rate_to_gbp('USD')) | |
raised = int(total) | |
if raised >= target: | |
pct = 100 | |
else: | |
pct = (raised * 100)/target | |
kwargs = dict( | |
backers=backers, | |
campaign=campaign_id, | |
email=PAYPAL_ACCOUNT, | |
info=escape(campaign[3]), | |
pct=pct * 1, # multiply by 2 if width == 200px, etc. | |
raised=render_number_with_commas(raised/100), | |
target=render_number_with_commas(target/100), | |
title=escape(campaign[2]) | |
) | |
out("""<div class="campaign"> | |
<div class="campaign-title">%(title)s</div> | |
<div class="campaign-info">%(info)s</div> | |
<div class="campaign-backers">%(backers)s</div> | |
<div class="campaign-raised">Raised: £%(raised)s</div> | |
<div class="campaign-target">%(pct)s%% of target £%(target)s</div> | |
<form action="https://www.paypal.com/uk/cgi-bin/webscr" method="post"> | |
<input type="hidden" name="charset" value="utf-8"> | |
<input type="hidden" name="cmd" value="_donations"> | |
<input type="hidden" name="business" value="%(email)s"> | |
<input type="hidden" name="item_name" value="Fund WikiHouse"> | |
<input type="hidden" name="item_number" value="wikihouse.%(campaign)s"> | |
<input type="hidden" name="custom" value="wikihouse.%(campaign)s"> | |
<input type="hidden" name="return" value="http://www.wikihouse.cc/thank-you"> | |
<input type="hidden" name="rm" value="1"> | |
<input type="hidden" name="cancel_return" value="http://www.wikihouse.cc/"> | |
<input type="hidden" name="notify_url" value="https://wikihouse-cc.appspot.com/ipn"> | |
<input type="text" name="amount" value="" placeholder="Amount, e.g. 30.00"> | |
<select name="currency_code"> | |
<option value="USD">US Dollars ($)</option> | |
<option value="GBP">British Pounds (£)</option> | |
<option value="EUR">Euros (€)</option> | |
</select> | |
<input type="submit" name="submit" value=" Back This! "> | |
</form></div>""" % kwargs) | |
return ''.join(html) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment