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

charge on Braintree #3470

Merged
merged 22 commits into from
May 28, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b223d4d
Start looking at charging on Braintree
chadwhitacre May 21, 2015
2a0ddaa
Basic test for fetch_card_holds
chadwhitacre May 21, 2015
de66ffe
Fixture to support test in previous commit
chadwhitacre May 21, 2015
89d6538
Fix create_card_hold tests to use Braintree
rohitpaulk May 21, 2015
54a923e
Group card hold tests by `create` and `capture`
rohitpaulk May 21, 2015
358d383
Fix create_card_hold to use Braintree
rohitpaulk May 21, 2015
d5c3c79
Fix capture_card_hold tests, and void card holds after tests
rohitpaulk May 21, 2015
a128256
group capture_card_hold tests properly
rohitpaulk May 21, 2015
7628138
fix capture_card_hold and cancel_card_hold
rohitpaulk May 21, 2015
87c0994
Use braintree cards in payday
rohitpaulk May 21, 2015
3a7edd9
Add `braintree_customer_id` to payday_participants
rohitpaulk May 21, 2015
07c86db
Handle braintree holds in payday
rohitpaulk May 21, 2015
db6bd23
fix tests in test_billing_payday.py
rohitpaulk May 21, 2015
38eaffa
Changes to fixtures
rohitpaulk May 21, 2015
a543a8a
Mark sync with balanced tests for debits as xfail
rohitpaulk May 21, 2015
06516aa
Update syncWithBalanced fixtures
rohitpaulk May 21, 2015
08f782d
Remove unused imports
rohitpaulk May 21, 2015
7153853
Perform braintree cleanup on every tearDown
rohitpaulk May 21, 2015
68187c5
Update Fixtures after last commit
rohitpaulk May 21, 2015
6d36b31
remove unused import
rohitpaulk May 21, 2015
98aacf9
Remove set_trace calls ;)
chadwhitacre May 28, 2015
71da515
Fix up a couple Balanced references in logs
chadwhitacre May 28, 2015
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
78 changes: 50 additions & 28 deletions gratipay/billing/exchanges.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from decimal import Decimal, ROUND_UP

import balanced
import braintree

from aspen import log
from aspen.utils import typecheck
Expand Down Expand Up @@ -170,27 +171,44 @@ def create_card_hold(db, participant, amount):
if participant.is_suspicious is not False:
raise NotWhitelisted # Participant not trusted.

route = ExchangeRoute.from_network(participant, 'balanced-cc')
route = ExchangeRoute.from_network(participant, 'braintree-cc')
if not route:
return None, 'No credit card'


# Go to Balanced.
# ===============
# Go to Braintree.
# ================

cents, amount_str, charge_amount, fee = _prep_hit(amount)
amount = charge_amount - fee
msg = "Holding " + amount_str + " on Balanced for " + username + " ... "
msg = "Holding " + amount_str + " on Braintree for " + username + " ... "

hold = None
error = ""
try:
card = thing_from_href('cards', route.address)
hold = card.hold( amount=cents
, description=username
, meta=dict(participant_id=participant.id, state='new')
)
log(msg + "succeeded.")
error = ""
result = braintree.Transaction.sale({
'amount': str(cents/100.0),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we in danger of float errors here? Why type is cents?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cents is an int.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You positive? I thought it was a Decimal here. Regardless, if Braintree accepts dollars we should just pass dollars. We only convert to cents in _prep_hit because Balanced wanted cents (though does ach_credit also use _prep_hit)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading correctly, cents comes from _prep_hit - and https://github.com/gratipay/gratipay.com/blob/master/gratipay/billing/exchanges.py#L288

We only convert to cents in _prep_hit because Balanced wanted cents

True. I wanted to minimize the code changes here, that's why I reused the function.

though does ach_credit also use _prep_hit

No, it doesn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to minimize the code changes here, that's why I reused the function.

Yeah, we can clean that up later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reticketed as #3500.

'customer_id': route.participant.braintree_customer_id,
'payment_method_token': route.address,
'options': { 'submit_for_settlement': False },
'custom_fields': {'participant_id': participant.id}
})

if result.is_success and result.transaction.status == 'authorized':
log(msg + "succeeded.")
error = ""
hold = result.transaction
elif result.is_success:
error = "Transaction status was %s" % result.transaction.status
else:
error = result.message

if error == '':
log(msg + "succeeded.")
else:
log(msg + "failed: %s" % error)
record_exchange(db, route, amount, fee, participant, 'failed', error)

except Exception as e:
error = repr_exception(e)
log(msg + "failed: %s" % error)
Expand All @@ -202,44 +220,48 @@ def create_card_hold(db, participant, amount):
def capture_card_hold(db, participant, amount, hold):
"""Capture the previously created hold on the participant's credit card.
"""
typecheck( hold, balanced.CardHold
typecheck( hold, braintree.Transaction
, amount, Decimal
)

username = participant.username
assert participant.id == int(hold.meta['participant_id'])
assert participant.id == int(hold.custom_fields['participant_id'])

route = ExchangeRoute.from_address(participant, 'balanced-cc', hold.card_href)
route = ExchangeRoute.from_address(participant, 'braintree-cc', hold.credit_card['token'])
assert isinstance(route, ExchangeRoute)

cents, amount_str, charge_amount, fee = _prep_hit(amount)
amount = charge_amount - fee # account for possible rounding
e_id = record_exchange(db, route, amount, fee, participant, 'pre')

meta = dict(participant_id=participant.id, exchange_id=e_id)
# TODO: Find a way to link transactions and corresponding exchanges
# meta = dict(participant_id=participant.id, exchange_id=e_id)

error = ''
try:
hold.capture(amount=cents, description=username, meta=meta)
record_exchange_result(db, e_id, 'succeeded', None, participant)
result = braintree.Transaction.submit_for_settlement(hold.id, str(cents/100.00))
assert result.is_success
if result.transaction.status != 'submitted_for_settlement':
error = result.transaction.status
except Exception as e:
error = repr_exception(e)
record_exchange_result(db, e_id, 'failed', error, participant)
raise

hold.meta['state'] = 'captured'
hold.save()

log("Captured " + amount_str + " on Balanced for " + username)
if error == '':
record_exchange_result(db, e_id, 'succeeded', None, participant)
log("Captured " + amount_str + " on Braintree for " + username)
else:
record_exchange_result(db, e_id, 'failed', error, participant)
raise Exception(error)


def cancel_card_hold(hold):
"""Cancel the previously created hold on the participant's credit card.
"""
hold.is_void = True
hold.meta['state'] = 'cancelled'
hold.save()
result = braintree.Transaction.void(hold.id)
assert result.is_success

amount = hold.amount / 100.0
participant_id = hold.meta['participant_id']
amount = hold.amount
participant_id = hold.custom_fields['participant_id']
log("Canceled a ${:.2f} hold for {}.".format(amount, participant_id))


Expand Down
25 changes: 8 additions & 17 deletions gratipay/billing/payday.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import itertools
from multiprocessing.dummy import Pool as ThreadPool

from balanced import CardHold
import braintree

import aspen.utils
from aspen import log
Expand Down Expand Up @@ -192,21 +192,12 @@ def prepare(cursor, ts_start):
def fetch_card_holds(participant_ids):
log('Fetching card holds.')
holds = {}
for hold in CardHold.query.filter(CardHold.f.meta.state == 'new'):
log_amount = hold.amount / 100.0
p_id = int(hold.meta['participant_id'])
state = 'new'
if hold.status == 'failed' or hold.failure_reason:
state = 'failed'
elif hold.voided_at:
state = 'cancelled'
elif getattr(hold, 'debit_href', None):
state = 'captured'
if state != 'new':
hold.meta['state'] = state
hold.save()
log('Set state to {} on a ${:.2f} hold for {}.'.format(state, log_amount, p_id))
continue
existing_holds = braintree.Transaction.search(
braintree.TransactionSearch.status == 'authorized'
)
for hold in existing_holds.items:
log_amount = hold.amount
p_id = int(hold.custom_fields['participant_id'])
if p_id in participant_ids:
log('Reusing a ${:.2f} hold for {}.'.format(log_amount, p_id))
holds[p_id] = hold
Expand Down Expand Up @@ -239,7 +230,7 @@ def f(p):
amount -= p.old_balance
if p.id in holds:
charge_amount = upcharge(amount)[0]
if holds[p.id].amount >= charge_amount * 100:
if holds[p.id].amount >= charge_amount:
return
else:
# The amount is too low, cancel the hold and make a new one
Expand Down
7 changes: 7 additions & 0 deletions gratipay/testing/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import braintree
from braintree.test.nonces import Nonces

from gratipay.billing.exchanges import cancel_card_hold
from gratipay.models.exchange_route import ExchangeRoute
from gratipay.testing import Harness
from gratipay.testing.vcr import use_cassette
Expand Down Expand Up @@ -49,6 +50,12 @@ def tearDownClass(cls):
for t in itertools.chain(credits, debits):
t.meta.pop('exchange_id')
t.save()
# Braintree Cleanup
existing_holds = braintree.Transaction.search(
braintree.TransactionSearch.status == 'authorized'
)
for hold in existing_holds.items:
cancel_card_hold(hold)
super(BillingHarness, cls).tearDownClass()


Expand Down
3 changes: 2 additions & 1 deletion sql/payday.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ CREATE TABLE payday_participants AS
, ( SELECT count(*)
FROM exchange_routes r
WHERE r.participant = p.id
AND network = 'balanced-cc'
AND network = 'braintree-cc'
) > 0 AS has_credit_card
, braintree_customer_id
FROM participants p
WHERE is_suspicious IS NOT true
AND claimed_time < %(ts_start)s
Expand Down
Loading