This repository has been archived by the owner on Feb 8, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 308
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4598 from gratipay/new-homepage
"Pay for Open Source" form on homepage
- Loading branch information
Showing
43 changed files
with
1,854 additions
and
109 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
BEGIN; | ||
CREATE TYPE follow_up AS ENUM ('monthly', 'quarterly', 'yearly', 'never'); | ||
CREATE TABLE payments_for_open_source | ||
( uuid text PRIMARY KEY | ||
, ctime timestamptz NOT NULL DEFAULT now() | ||
|
||
-- card charge | ||
, amount bigint NOT NULL | ||
, braintree_transaction_id text UNIQUE DEFAULT NULL | ||
, braintree_result_message text DEFAULT NULL | ||
|
||
-- contact info | ||
, name text NOT NULL | ||
, follow_up follow_up NOT NULL | ||
, email_address text NOT NULL | ||
|
||
-- promotion details | ||
, promotion_name text NOT NULL DEFAULT '' | ||
, promotion_url text NOT NULL DEFAULT '' | ||
, promotion_twitter text NOT NULL DEFAULT '' | ||
, promotion_message text NOT NULL DEFAULT '' | ||
); | ||
END; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{{ _("Invoice from Gratipay") }} | ||
|
||
[---] text/html | ||
{{ _( "Thank you for your payment of {amount} for open source!" | ||
, amount=format_currency(amount, 'USD') | ||
) }} | ||
<br> | ||
<br> | ||
<a href="{{ invoice_url }}" style="{{ button_style }}">{{ _("View Invoice") }}</a> | ||
|
||
[---] text/plain | ||
{{ _( "Thank you for your payment of {amount} for open source!" | ||
, amount=format_currency(amount, 'USD') | ||
) }} | ||
|
||
{{ _("Follow this link to view your invoice:") }} | ||
|
||
{{ invoice_url }} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# -*- coding: utf-8 -*- | ||
from __future__ import absolute_import, division, print_function, unicode_literals | ||
|
||
import braintree | ||
from uuid import uuid4 | ||
from decimal import Decimal as D | ||
|
||
|
||
class CardCharger(object): | ||
|
||
def __init__(self, online=False): | ||
self.implementation = Braintree() if online else FakeBraintree() | ||
|
||
def charge(self, params): | ||
return self.implementation.charge(params) | ||
|
||
|
||
# Online | ||
# ====== | ||
|
||
class Braintree(object): | ||
"""Sends data to Braintree. | ||
""" | ||
|
||
def charge(self, params): | ||
"""Charge using the Braintree APi, returning a result. | ||
""" | ||
return braintree.Transaction.sale(params) | ||
|
||
|
||
# Offline | ||
# ======= | ||
|
||
class FakeTransaction(object): | ||
def __init__(self): | ||
self.id = uuid4().hex | ||
|
||
class FakeSuccessResult(object): | ||
def __init__(self): | ||
self.is_success = True | ||
self.transaction = FakeTransaction() | ||
|
||
class FakeFailureResult(object): | ||
def __init__(self): | ||
self.is_success = False | ||
self.message = 'Not a success.' | ||
self.transaction = FakeTransaction() | ||
|
||
class FakeErrorResult(object): | ||
def __init__(self): | ||
self.is_success = False | ||
self.message = 'Not even a success.' | ||
self.transaction = None | ||
|
||
|
||
class FakeBraintree(object): | ||
"""For offline use. | ||
""" | ||
|
||
def charge(self, params): | ||
"""Return a fake result. Partially implements Braintree's testing logic: | ||
- fake-valid-nonce returns a success result | ||
- amount >= 2000 returns a failure result | ||
- otherwise return an error result | ||
https://developers.braintreepayments.com/reference/general/testing/python | ||
""" | ||
if params['payment_method_nonce'] == 'fake-valid-nonce': | ||
if D(params['amount']) < 2000: | ||
return FakeSuccessResult() | ||
return FakeFailureResult() | ||
return FakeErrorResult() |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# -*- coding: utf-8 -*- | ||
"""This is the Python library behind gratipay.com. | ||
""" | ||
from __future__ import absolute_import, division, print_function, unicode_literals | ||
|
||
from gratipay import utils | ||
from gratipay.models.payment_for_open_source import PaymentForOpenSource | ||
|
||
|
||
def _parse(raw): | ||
"""Given a POST request.body, return (parsed<dict>, errors<list>). | ||
""" | ||
|
||
errors = [] | ||
x = lambda f: raw[f].strip() # KeyError -> 400 | ||
|
||
# amount | ||
amount = x('amount') | ||
if (not amount.isdigit()) or (int(amount) < 10): | ||
errors.append('amount') | ||
amount = ''.join(x for x in amount.split('.')[0] if x.isdigit()) | ||
|
||
# credit card nonce | ||
payment_method_nonce = x('payment_method_nonce') | ||
if len(payment_method_nonce) > 36: | ||
errors.append('payment_method_nonce') | ||
payment_method_nonce = '' | ||
|
||
# name | ||
name = x('name') | ||
if len(name) > 255: | ||
name = name[:255] | ||
errors.append('name') | ||
|
||
# email address | ||
email_address = x('email_address') | ||
if email_address and not utils.is_valid_email_address(email_address): | ||
email_address = email_address[:255] | ||
errors.append('email_address') | ||
|
||
# follow_up | ||
follow_up = x('follow_up') | ||
if follow_up not in ('monthly', 'quarterly', 'yearly', 'never'): | ||
follow_up = 'monthly' | ||
errors.append('follow_up') | ||
|
||
promotion_name = x('promotion_name') | ||
if len(promotion_name) > 32: | ||
promotion_name = promotion_name[:32] | ||
errors.append('promotion_name') | ||
|
||
promotion_url = x('promotion_url') | ||
is_link = lambda x: (x.startswith('http://') or x.startswith('https://')) and '.' in x | ||
if len(promotion_url) > 255 or (promotion_url and not is_link(promotion_url)): | ||
promotion_url = promotion_url[:255] | ||
errors.append('promotion_url') | ||
|
||
promotion_twitter = x('promotion_twitter') | ||
if len(promotion_twitter) > 32: | ||
promotion_twitter = promotion_twitter[:32] | ||
# TODO What are Twitter's rules? | ||
errors.append('promotion_twitter') | ||
|
||
promotion_message = x('promotion_message') | ||
if len(promotion_message) > 128: | ||
promotion_message = promotion_message[:128] | ||
errors.append('promotion_message') | ||
|
||
parsed = { 'amount': amount | ||
, 'payment_method_nonce': payment_method_nonce | ||
, 'name': name | ||
, 'email_address': email_address | ||
, 'follow_up': follow_up | ||
, 'promotion_name': promotion_name | ||
, 'promotion_url': promotion_url | ||
, 'promotion_twitter': promotion_twitter | ||
, 'promotion_message': promotion_message | ||
} | ||
return parsed, errors | ||
|
||
|
||
def _store(parsed): | ||
return PaymentForOpenSource.insert(**parsed) | ||
|
||
|
||
def _charge(app, pfos, nonce): | ||
params = { 'amount': pfos.amount | ||
, 'payment_method_nonce': nonce | ||
, 'options': {'submit_for_settlement': True} | ||
, 'custom_fields': {'pfos_uuid': pfos.uuid} | ||
} | ||
result = app.pfos_card_charger.charge(params) | ||
pfos.process_result(result) | ||
|
||
|
||
def _send(app, pfos): | ||
app.email_queue.put( to=None | ||
, template='paid-for-open-source' | ||
, email=pfos.email_address | ||
, amount=pfos.amount | ||
, invoice_url=pfos.invoice_url | ||
) | ||
|
||
|
||
def pay_for_open_source(app, raw): | ||
parsed, errors = _parse(raw) | ||
out = {'errors': errors, 'invoice_url': None} | ||
if not errors: | ||
payment_method_nonce = parsed.pop('payment_method_nonce') | ||
pfos = _store(parsed) | ||
_charge(app, pfos, payment_method_nonce) | ||
if pfos.succeeded: | ||
out['invoice_url'] = pfos.invoice_url | ||
if pfos.email_address: | ||
_send(app, pfos) | ||
else: | ||
out['errors'].append('charging') | ||
return out |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# -*- coding: utf-8 -*- | ||
from __future__ import absolute_import, division, print_function, unicode_literals | ||
|
||
import gratipay | ||
from uuid import uuid4 | ||
from postgres.orm import Model | ||
|
||
|
||
class PaymentForOpenSource(Model): | ||
|
||
typname = "payments_for_open_source" | ||
|
||
def __repr__(self): | ||
return '<PaymentForOpenSource: {}>'.format(repr(self.amount)) | ||
|
||
|
||
@property | ||
def succeeded(self): | ||
return self.braintree_result_message == '' | ||
|
||
|
||
@property | ||
def invoice_url(self): | ||
if not self.succeeded: | ||
return None | ||
return '{}/browse/payments/{}/invoice.html'.format(gratipay.base_url, self.uuid) | ||
|
||
|
||
@classmethod | ||
def from_uuid(cls, uuid, cursor=None): | ||
"""Take a uuid and return an object. | ||
""" | ||
return (cursor or cls.db).one(""" | ||
SELECT pfos.*::payments_for_open_source | ||
FROM payments_for_open_source pfos | ||
WHERE uuid = %s | ||
""", (uuid,)) | ||
|
||
|
||
@classmethod | ||
def insert(cls, amount, name, follow_up, email_address, | ||
promotion_name, promotion_url, promotion_twitter, promotion_message, | ||
cursor=None): | ||
"""Take baseline info and insert into the database. | ||
""" | ||
uuid = uuid4().hex | ||
return (cursor or cls.db).one(""" | ||
INSERT INTO payments_for_open_source | ||
(uuid, amount, name, follow_up, email_address, | ||
promotion_name, promotion_url, promotion_twitter, promotion_message) | ||
VALUES (%s, %s, %s, %s, %s, | ||
%s, %s, %s, %s) | ||
RETURNING payments_for_open_source.*::payments_for_open_source | ||
""", (uuid, amount, name, follow_up, email_address, | ||
promotion_name, promotion_url, promotion_twitter, promotion_message)) | ||
|
||
|
||
def process_result(self, result): | ||
"""Take a Braintree API result and update the database. | ||
""" | ||
result_message = '' if result.is_success else result.message | ||
transaction_id = None | ||
if result.transaction: | ||
transaction_id = result.transaction.id | ||
|
||
# Verify that Braintree is sending us the right payload. | ||
# TODO This is hard to test and it should be a pretty tight guarantee, | ||
# so I am commenting out for now. :( | ||
#pfos_uuid = result.transaction.custom_fields['pfos_uuid'] | ||
#assert pfos_uuid == self.uuid, (pfos_uuid, transaction_id) | ||
|
||
self.db.run(""" | ||
UPDATE payments_for_open_source | ||
SET braintree_result_message=%s | ||
, braintree_transaction_id=%s | ||
WHERE uuid=%s | ||
""", (result_message, transaction_id, self.uuid)) | ||
self.set_attributes( braintree_result_message=result_message | ||
, braintree_transaction_id=transaction_id | ||
) |
Oops, something went wrong.