Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Payments for open source receipt #4611

Merged
merged 4 commits into from
Sep 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ before_script:
- echo "EMAIL_QUEUE_LOG_METRICS_EVERY=0" >> local.env

- psql -U postgres -c 'CREATE DATABASE "gratipay";'
- if [ "${TRAVIS_BRANCH}" = "master" -a "${TRAVIS_PULL_REQUEST}" = "false" ]; then rm -rfv tests/py/fixtures; fi
- if [ "${TRAVIS_BRANCH}" = "master" -a "${TRAVIS_PULL_REQUEST}" = "false" ]; then
rm -rfv tests/py/fixtures;
echo "LOAD_BRAINTREE_FORM_ON_HOMEPAGE=yes" >> local.env;
fi
script: LD_LIBRARY_PATH=/usr/local/lib xvfb-run make test-schema bgrun test doc
notifications:
email: false
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ bin_dir := $(shell $(python) -c 'import sys; bin = "Scripts" if sys.platform ==
env_bin := env/$(bin_dir)
venv := "./vendor/virtualenv-15.1.0.py"
doc_env_files := defaults.env,docs/doc.env,docs/local.env
test_env_files := defaults.env,tests/defaults.env,tests/local.env
test_env_files := defaults.env,local.env,tests/defaults.env,tests/local.env
pip := $(env_bin)/pip
honcho := $(env_bin)/honcho
honcho_run := $(honcho) run -e defaults.env,local.env
Expand Down
2 changes: 2 additions & 0 deletions defaults.env
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,5 @@ PROJECT_REVIEW_USERNAME=
PROJECT_REVIEW_TOKEN=

RAISE_SIGNIN_NOTIFICATIONS=no

LOAD_BRAINTREE_FORM_ON_HOMEPAGE=no
13 changes: 6 additions & 7 deletions emails/paid-for-open-source.spt
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
{{ _("Payment for open source") }}
{{ _("Invoice from Gratipay") }}

[---] text/html
{{ _( "Congratulations! You now have a moral license to use community-built open source software, because you paid {amount} to the open source community through Gratipay. Thank you for your purchase!"
{{ _( "Thank you for your payment of {amount} for open source!"
, amount=format_currency(amount, 'USD')
) }}
<br>
<br>
<a href="{{ receipt_url }}"
style="{{ button_style }}">{{ _("View Receipt") }}</a>
<a href="{{ invoice_url }}" style="{{ button_style }}">{{ _("View Invoice") }}</a>

[---] text/plain
{{ _( "Congratulations! You now have a moral license to use community-built open source software, because you paid {amount} to the open source community through Gratipay. Thank you for your purchase!"
{{ _( "Thank you for your payment of {amount} for open source!"
, amount=format_currency(amount, 'USD')
) }}

{{ _("Follow this link to view your receipt:") }}
{{ _("Follow this link to view your invoice:") }}

{{ receipt_url }}
{{ invoice_url }}
2 changes: 2 additions & 0 deletions gratipay/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from . import email, sync_npm, utils
from .cron import Cron
from .models import GratipayDB
from .card_charger import CardCharger
from .payday_runner import PaydayRunner
from .project_review_process import ProjectReviewProcess
from .website import Website
Expand Down Expand Up @@ -47,6 +48,7 @@ def __init__(self):
self.website = website
self.payday_runner = PaydayRunner(self)
self.project_review_process = ProjectReviewProcess(env, db, self.email_queue)
self.pfos_card_charger = CardCharger(online=env.load_braintree_form_on_homepage)


def install_periodic_jobs(self, website, env, db):
Expand Down
74 changes: 74 additions & 0 deletions gratipay/card_charger.py
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()
31 changes: 15 additions & 16 deletions gratipay/homepage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""
from __future__ import absolute_import, division, print_function, unicode_literals

import braintree
from gratipay import utils
from gratipay.models.payment_for_open_source import PaymentForOpenSource

Expand All @@ -29,8 +28,8 @@ def _parse(raw):

# name
name = x('name')
if len(name) > 256:
name = name[:256]
if len(name) > 255:
name = name[:255]
errors.append('name')

# email address
Expand All @@ -52,8 +51,8 @@ def _parse(raw):

promotion_url = x('promotion_url')
is_link = lambda x: (x.startswith('http://') or x.startswith('https://')) and '.' in x
if len(promotion_url) > 256 or (promotion_url and not is_link(promotion_url)):
promotion_url = promotion_url[:256]
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')
Expand Down Expand Up @@ -84,13 +83,13 @@ def _store(parsed):
return PaymentForOpenSource.insert(**parsed)


def _charge(pfos, payment_method_nonce):
charge = braintree.Transaction.sale
result = charge({ 'amount': pfos.amount
, 'payment_method_nonce': payment_method_nonce
, 'options': {'submit_for_settlement': True}
, 'custom_fields': {'pfos_uuid': pfos.uuid}
})
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)


Expand All @@ -99,19 +98,19 @@ def _send(app, pfos):
, template='paid-for-open-source'
, email=pfos.email_address
, amount=pfos.amount
, receipt_url=pfos.receipt_url
, invoice_url=pfos.invoice_url
)


def pay_for_open_source(app, raw):
parsed, errors = _parse(raw)
out = {'errors': errors, 'receipt_url': None}
out = {'errors': errors, 'invoice_url': None}
if not errors:
payment_method_nonce = parsed.pop('payment_method_nonce')
pfos = _store(parsed)
_charge(pfos, payment_method_nonce)
_charge(app, pfos, payment_method_nonce)
if pfos.succeeded:
out['receipt_url'] = pfos.receipt_url
out['invoice_url'] = pfos.invoice_url
if pfos.email_address:
_send(app, pfos)
else:
Expand Down
4 changes: 2 additions & 2 deletions gratipay/models/payment_for_open_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ def succeeded(self):


@property
def receipt_url(self):
def invoice_url(self):
if not self.succeeded:
return None
return '{}/browse/payments/{}/receipt.html'.format(gratipay.base_url, self.uuid)
return '{}/browse/payments/{}/invoice.html'.format(gratipay.base_url, self.uuid)


@classmethod
Expand Down
1 change: 1 addition & 0 deletions gratipay/wireup.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ def env():
PROJECT_REVIEW_USERNAME = unicode,
PROJECT_REVIEW_TOKEN = unicode,
RAISE_SIGNIN_NOTIFICATIONS = is_yesish,
LOAD_BRAINTREE_FORM_ON_HOMEPAGE = is_yesish,
GUNICORN_OPTS = unicode,
)

Expand Down
70 changes: 48 additions & 22 deletions js/gratipay/homepage.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,76 @@
Gratipay.homepage = {}

Gratipay.homepage.initForm = function(clientAuthorization) {
$form = $('#homepage #content form');
var self = this;
self.$form = $('#homepage #content form');
self.$submit = self.$form.find('button[type=submit]');

function callback(createErr, instance) {
$submit = $form.find('button[type=submit]');
$submit.click(function(e) {
if (clientAuthorization === undefined) { // Offline mode

$('#braintree-container').addClass('offline').html(Gratipay.jsonml(['div',
['div', {'class': 'field amount'},
['label', {'for': 'nonce'}, 'Nonce'],
['input', {'id': 'nonce', 'value': 'fake-valid-nonce', 'required': true}, 'Nonce'],
],
['p', {'class': 'fine-print'}, "If you're seeing this on gratipay.com, we screwed up."]
]));

self.$submit.click(function(e) {
e.preventDefault();
instance.requestPaymentMethod(function(requestPaymentMethodErr, payload) {
Gratipay.homepage.submitFormWithNonce(payload.nonce);
});
nonce = $('#braintree-container input').val();
self.submitFormWithNonce(nonce);
});
}

braintree.dropin.create({
authorization: clientAuthorization,
container: '#braintree-container'
}, callback);
} else { // Online mode (sandbox or production)

function braintreeInitCallback(createErr, instance) {
if (createErr) {
$('#braintree-container').addClass('failed').text('Failed to load Braintree.');
} else {
self.$submit.click(function(e) {
e.preventDefault();
instance.requestPaymentMethod(function(requestPaymentMethodErr, payload) {
self.submitFormWithNonce(payload.nonce);
});
});
}
}

braintree.dropin.create({
authorization: clientAuthorization,
container: '#braintree-container'
}, braintreeInitCallback);
}
};


Gratipay.homepage.submitFormWithNonce = function(nonce) {
$submit = $form.find('button[type=submit]');
$form = $('#homepage #content form');
var data = new FormData($form[0]);
var self = this;
var data = new FormData(self.$form[0]);
data.set('payment_method_nonce', nonce);

$submit.prop('disable', true);
self.$submit.prop('disable', true);

$.ajax({
url: $form.attr('action'),
url: self.$form.attr('action'),
type: 'POST',
data: data,
processData: false,
contentType: false,
dataType: 'json',
success: function(data) {
console.log(data);
// Due to Aspen limitations we use 200 for both success and failure. :/
if (data.errors.length > 0) {
$submit.prop('disable', false);
self.$submit.prop('disable', false);
Gratipay.notification(data.msg, 'error');
for (var i=0, fieldName; fieldName=data.errors[i]; i++) {
$('.'+fieldName, self.$form).addClass('error');
}
} else {
$('.payment-complete a.receipt').attr('href', data.receipt_url);
$('.payment-complete').slideDown(200);
$('form').slideUp(500);
$('.payment-complete a.invoice').attr('href', data.invoice_url);
$('form').slideUp(500, function() {
$('.payment-complete').fadeIn(500);
});
}
}
});
Expand Down
Loading