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

Vault new cards at Braintree #3389

Merged
merged 26 commits into from
May 12, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
98070d0
Add braintree to vendor/
rohitpaulk May 8, 2015
4cd8c0e
wireup braintree
rohitpaulk May 8, 2015
a64aca1
Load Balanced JS for bank form, and Braintree for cc
rohitpaulk May 8, 2015
1301707
Add 'braintree-cc' as a payment network
rohitpaulk May 8, 2015
4260b3d
Handle braintree nonce in routes/associate.json.spt
rohitpaulk May 8, 2015
6b365dc
Modify payments.cc.* to handle braintree
rohitpaulk May 8, 2015
1d3be33
Implement invalidation of braintree cards
rohitpaulk May 8, 2015
59ca2f3
Use braintree_customer_id
rohitpaulk May 8, 2015
374b141
Add get_braintree_account and get_braintree_token
rohitpaulk May 8, 2015
6cf3e74
Add braintree token to credit card form
rohitpaulk May 8, 2015
c9d1bbe
Backward compatibility for balanced cards
rohitpaulk May 8, 2015
6d8b0a1
Use a separate sandbox account for tests
rohitpaulk May 8, 2015
713247a
Fix credit card page for Braintree cards
rohitpaulk May 9, 2015
4fca7e0
Backward compatibility for invalidation
rohitpaulk May 9, 2015
39567d0
Fix credit card page for Balanced cards
rohitpaulk May 9, 2015
f0c2950
Update test fixtures
rohitpaulk May 9, 2015
f1b2814
Rename BalancedHarness to BillingHarness
rohitpaulk May 9, 2015
3d020dc
Don't actually create card for dummy exchanges
rohitpaulk May 10, 2015
887222b
Add braintree customer to BillingHarness
rohitpaulk May 10, 2015
a268ffa
Add test for associate a new balanced card (should fail)
rohitpaulk May 10, 2015
0b88399
Add tests for braintree cards
rohitpaulk May 10, 2015
e51c845
Rename participants in BillingHarness to avoid conflicts
rohitpaulk May 10, 2015
c4bbe41
add tests for credit card page
rohitpaulk May 10, 2015
4a99046
Fix receipt page for braintree cards
rohitpaulk May 10, 2015
f779d0b
Restore all fixtures
rohitpaulk May 10, 2015
031f1ea
Merge remote-tracking branch 'origin/master' into braintree
rohitpaulk May 10, 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
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