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

Commit

Permalink
Merge pull request #3389 from gratipay/braintree
Browse files Browse the repository at this point in the history
Vault new cards at Braintree
  • Loading branch information
chadwhitacre committed May 12, 2015
2 parents cd7305d + 031f1ea commit 87d3a68
Show file tree
Hide file tree
Showing 36 changed files with 3,005 additions and 3,988 deletions.
5 changes: 5 additions & 0 deletions defaults.env
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ GRATIPAY_COMPRESS_ASSETS=no

BALANCED_API_SECRET=ak-test-2zFgxfVijBzn4xC9dLRtLkxoB38iNKNKR

BRAINTREE_SANDBOX_MODE=true
BRAINTREE_MERCHANT_ID=ddnq29fv74cqxwkg
BRAINTREE_PUBLIC_KEY=f9xk4pb5ts86k67k
BRAINTREE_PRIVATE_KEY=36ded60b7f4a43ebc605ca5f4b33d909

COINBASE_API_KEY=uETKVUrnPuXzVaVj
COINBASE_API_SECRET=32zAkQCcHHYkGGn29VkvEZvn21PM1lgO

Expand Down
49 changes: 49 additions & 0 deletions gratipay/billing/instruments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import braintree
import balanced

class CreditCard:
def __init__(self, *args, **kwargs):
fields = [
'card_type',
'number',
'expiration_year',
'expiration_month',
'address_line1',
'address_line2',
'address_city',
'address_state',
'address_postal_code',
'address_country_code',
'cardholder_name'
]
for field in fields:
setattr(self, field, kwargs.pop(field, ''))

@classmethod
def from_route(cls, route):
if route.network == 'braintree-cc':
card = braintree.PaymentMethod.find(route.address)
return cls(
card_type=card.card_type,
number=card.masked_number,
expiration_month=card.expiration_month,
expiration_year=card.expiration_year,
cardholder_name=card.cardholder_name,
address_postal_code=card.billing_address.postal_code
)
else:
assert route.network == 'balanced-cc'
card = balanced.Card.fetch(route.address)
return cls(
card_type=card.brand,
number=card.number,
expiration_month=card.expiration_month,
expiration_year=card.expiration_year,
cardholder_name=card.name,
address_line1=card.address['line1'],
address_line2=card.address['line2'],
address_city=card.address['city'],
address_state=card.address['state'],
address_postal_code=card.address['postal_code'],
address_country_code=card.address['country_code'],
)
14 changes: 8 additions & 6 deletions gratipay/models/exchange_route.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals

import balanced
import braintree
from postgres.orm import Model


Expand Down Expand Up @@ -74,12 +75,13 @@ def insert(cls, participant, network, address, error='', fee_cap=None):
return r

def invalidate(self):
if self.network.startswith('balanced-'):
if self.network == 'balanced-cc':
balanced.Card.fetch(self.address).unstore()
else:
assert self.network == 'balanced-ba'
balanced.BankAccount.fetch(self.address).delete()
if self.network == 'balanced-ba':
balanced.BankAccount.fetch(self.address).delete()
elif self.network == 'balanced-cc':
balanced.Card.fetch(self.address).unstore()
elif self.network == 'braintree-cc':
braintree.PaymentMethod.delete(self.address)

self.update_error('invalidated')

def update_error(self, new_error, propagate=True):
Expand Down
34 changes: 33 additions & 1 deletion gratipay/models/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from aspen.utils import utcnow
import balanced
import braintree
from dependency_injection import resolve_dependencies
from markupsafe import escape as htmlescape
from postgres.orm import Model
Expand Down Expand Up @@ -774,7 +775,11 @@ def get_bank_account_error(self):
return getattr(ExchangeRoute.from_network(self, 'balanced-ba'), 'error', None)

def get_credit_card_error(self):
return getattr(ExchangeRoute.from_network(self, 'balanced-cc'), 'error', None)
if self.braintree_customer_id:
return getattr(ExchangeRoute.from_network(self, 'braintree-cc'), 'error', None)
# Backward compatibility until we get rid of balanced
else:
return getattr(ExchangeRoute.from_network(self, 'balanced-cc'), 'error', None)

def get_cryptocoin_addresses(self):
routes = self.db.all("""
Expand Down Expand Up @@ -1364,6 +1369,33 @@ def get_balanced_account(self):
customer = balanced.Customer.fetch(self.balanced_customer_href)
return customer

def get_braintree_account(self):
"""Fetch or create a braintree account for this participant.
"""
if not self.braintree_customer_id:
customer = braintree.Customer.create({
'custom_fields': {'participant_id': self.id}
}).customer

r = self.db.one("""
UPDATE participants
SET braintree_customer_id=%s
WHERE id=%s
AND braintree_customer_id IS NULL
RETURNING id
""", (customer.id, self.id))

if not r:
return self.get_braintree_account()
else:
customer = braintree.Customer.find(self.braintree_customer_id)
return customer

def get_braintree_token(self):
account = self.get_braintree_account()

token = braintree.ClientToken.generate({'customer_id': account.id})
return token

class StillReceivingTips(Exception): pass
class BalanceIsNotZero(Exception): pass
Expand Down
3 changes: 1 addition & 2 deletions gratipay/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,7 @@ def make_exchange(self, route, amount, fee, participant, status='succeeded', err
network = route
route = ExchangeRoute.from_network(participant, network)
if not route:
from .balanced import BalancedHarness
route = ExchangeRoute.insert(participant, network, BalancedHarness.card_href)
route = ExchangeRoute.insert(participant, network, 'dummy-address')
assert route
e_id = record_exchange(self.db, route, amount, fee, participant, 'pre')
record_exchange_result(self.db, e_id, status, error, participant)
Expand Down
36 changes: 32 additions & 4 deletions gratipay/testing/balanced.py → gratipay/testing/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,44 @@
import itertools

import balanced
import braintree
from braintree.test.nonces import Nonces

from gratipay.models.exchange_route import ExchangeRoute
from gratipay.testing import Harness
from gratipay.testing.vcr import use_cassette


class BalancedHarness(Harness):
class BillingHarness(Harness):

def setUp(self):
# Balanced Customer without funding instruments
self.david = self.make_participant('david', is_suspicious=False,
claimed_time='now',
balanced_customer_href=self.david_href)

# Balanced Customer with CC attached
self.janet = self.make_participant('janet', is_suspicious=False,
claimed_time='now',
balanced_customer_href=self.janet_href)
self.janet_route = ExchangeRoute.insert(self.janet, 'balanced-cc', self.card_href)

# Balanced Customer with BA attached
self.homer = self.make_participant('homer', is_suspicious=False,
claimed_time='now',
balanced_customer_href=self.homer_href)
self.homer_route = ExchangeRoute.insert(self.homer, 'balanced-ba', self.bank_account_href)

# Braintree Customer without funding instruments
self.roman = self.make_participant('roman', is_suspicious=False,
claimed_time='now',
braintree_customer_id=self.roman_bt_id)
# Braintree Customer with CC attached
self.obama = self.make_participant('obama', is_suspicious=False,
claimed_time='now',
braintree_customer_id=self.obama_bt_id)
self.obama_route = ExchangeRoute.insert(self.obama, 'braintree-cc', self.obama_cc_token)

@classmethod
def tearDownClass(cls):
has_exchange_id = balanced.Transaction.f.meta.contains('exchange_id')
Expand All @@ -32,11 +49,11 @@ def tearDownClass(cls):
for t in itertools.chain(credits, debits):
t.meta.pop('exchange_id')
t.save()
super(BalancedHarness, cls).tearDownClass()
super(BillingHarness, cls).tearDownClass()


with use_cassette('BalancedHarness'):
cls = BalancedHarness
with use_cassette('BillingHarness'):
cls = BillingHarness
balanced.configure(balanced.APIKey().save().secret)
mp = balanced.Marketplace.my_marketplace
if not mp:
Expand Down Expand Up @@ -74,3 +91,14 @@ def tearDownClass(cls):
).save()
cls.bank_account.associate_to_customer(cls.homer_href)
cls.bank_account_href = unicode(cls.bank_account.href)

cls.roman_bt_id = braintree.Customer.create().customer.id

cls.obama_bt_id = braintree.Customer.create().customer.id

cls.bt_card = braintree.PaymentMethod.create({
"customer_id": cls.obama_bt_id,
"payment_method_nonce": Nonces.Transactable
}).payment_method

cls.obama_cc_token = cls.bt_card.token
16 changes: 16 additions & 0 deletions gratipay/wireup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from babel.messages.pofile import read_po
from babel.numbers import parse_pattern
import balanced
import braintree
import gratipay
import gratipay.billing.payday
import raven
Expand Down Expand Up @@ -68,6 +69,17 @@ def mail(env, project_root='.'):
def billing(env):
balanced.configure(env.balanced_api_secret)

if env.braintree_sandbox_mode:
braintree_env = braintree.Environment.Sandbox
else:
braintree_env = braintree.Environment.Production

braintree.Configuration.configure(
braintree_env,
env.braintree_merchant_id,
env.braintree_public_key,
env.braintree_private_key
)

def username_restrictions(website):
gratipay.RESTRICTED_USERNAMES = os.listdir(website.www_root)
Expand Down Expand Up @@ -344,6 +356,10 @@ def env():
GRATIPAY_CACHE_STATIC = is_yesish,
GRATIPAY_COMPRESS_ASSETS = is_yesish,
BALANCED_API_SECRET = unicode,
BRAINTREE_SANDBOX_MODE = is_yesish,
BRAINTREE_MERCHANT_ID = unicode,
BRAINTREE_PUBLIC_KEY = unicode,
BRAINTREE_PRIVATE_KEY = unicode,
GITHUB_CLIENT_ID = unicode,
GITHUB_CLIENT_SECRET = unicode,
GITHUB_CALLBACK = unicode,
Expand Down
Loading

0 comments on commit 87d3a68

Please sign in to comment.