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

Balanced rev1.1 #2036

Merged
merged 12 commits into from Feb 20, 2014
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -5,6 +5,7 @@ install: make env node_modules
before_script:
- echo "DATABASE_URL=dbname=gittip" | tee -a tests/local.env local.env
- psql -U postgres -c 'CREATE DATABASE "gittip";'
- rm -rf tests/fixtures
script: make test
notifications:
email: false
Expand Down
4 changes: 3 additions & 1 deletion Gruntfile.js
Expand Up @@ -100,7 +100,9 @@ module.exports = function(grunt) {
if (!started && /Greetings, program! Welcome to port 8537\./.test(data)) {
started = true;
grunt.log.writeln('started.');
done();
setTimeout(done, 1000);
} else if (started && /Is something already running on port 8537/.test(data)) {
started = false;
} else
stdout.push(data);
});
Expand Down
168 changes: 70 additions & 98 deletions gittip/billing/__init__.py
Expand Up @@ -32,31 +32,21 @@ def get_balanced_account(db, username, balanced_account_uri):
, balanced_account_uri, (unicode, None)
)

# XXX Balanced requires an email address
# https://github.com/balanced/balanced-api/issues/20
# quote to work around https://github.com/gittip/www.gittip.com/issues/781
email_address = '{}@gittip.com'.format(quote(username))


if balanced_account_uri is None:
try:
account = \
balanced.Account.query.filter(email_address=email_address).one()
except balanced.exc.NoResultFound:
account = balanced.Account(email_address=email_address).save()
customer = balanced.Customer(meta={
'username': username,
}).save()
BALANCED_ACCOUNT = """\

UPDATE participants
SET balanced_account_uri=%s
WHERE username=%s

"""
db.run(BALANCED_ACCOUNT, (account.uri, username))
account.meta['username'] = username
account.save() # HTTP call under here
db.run(BALANCED_ACCOUNT, (customer.href, username))
else:
account = balanced.Account.find(balanced_account_uri)
return account
customer = balanced.Customer.fetch(balanced_account_uri)
return customer


def associate(db, thing, username, balanced_account_uri, balanced_thing_uri):
Expand All @@ -71,33 +61,35 @@ def associate(db, thing, username, balanced_account_uri, balanced_thing_uri):

"""
typecheck( username, unicode
, balanced_account_uri, (unicode, None, balanced.Account)
, balanced_thing_uri, unicode
, thing, unicode
, balanced_account_uri, (unicode, None, balanced.Customer)
, balanced_thing_uri, unicode
, thing, unicode
)

if isinstance(balanced_account_uri, balanced.Account):
if isinstance(balanced_account_uri, balanced.Customer):
balanced_account = balanced_account_uri
else:
balanced_account = get_balanced_account( db
, username
, balanced_account_uri
)
invalidate_on_balanced(thing, balanced_account.uri)
invalidate_on_balanced(thing, balanced_account.href)
SQL = "UPDATE participants SET last_%s_result=%%s WHERE username=%%s"
try:
if thing == "credit card":
SQL %= "bill"
obj = balanced.Card.fetch(balanced_thing_uri)
#add = balanced_account.add_card

if thing == "credit card":
add = balanced_account.add_card
SQL %= "bill"
else:
assert thing == "bank account", thing # sanity check
add = balanced_account.add_bank_account
SQL %= "ach"
else:
assert thing == "bank account", thing # sanity check
SQL %= "ach"
obj = balanced.BankAccount.fetch(balanced_thing_uri)
#add = balanced_account.add_bank_account

try:
add(balanced_thing_uri)
obj.associate_to_customer(balanced_account)
except balanced.exc.HTTPError as err:
error = err.message.decode('UTF-8') # XXX UTF-8?
error = err.message.message.decode('UTF-8') # XXX UTF-8?
Copy link
Contributor

Choose a reason for hiding this comment

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

message.message

3ec3960d0a1f1aacf42839c40e09d2a5_bigger

else:
error = ''
typecheck(error, unicode)
Expand All @@ -116,21 +108,19 @@ def invalidate_on_balanced(thing, balanced_account_uri):

"""
assert thing in ("credit card", "bank account")
typecheck(balanced_account_uri, unicode)
typecheck(balanced_account_uri, (str, unicode))

account = balanced.Account.find(balanced_account_uri)
things = account.cards if thing == "credit card" else account.bank_accounts
customer = balanced.Customer.fetch(balanced_account_uri)
things = customer.cards if thing == "credit card" else customer.bank_accounts

for _thing in things:
if _thing.is_valid:
_thing.is_valid = False
_thing.save()
_thing.unstore()


def clear(db, thing, username, balanced_account_uri):
typecheck( thing, unicode
, username, unicode
, balanced_account_uri, unicode
, balanced_account_uri, (unicode, str)
)
assert thing in ("credit card", "bank account"), thing
invalidate_on_balanced(thing, balanced_account_uri)
Expand Down Expand Up @@ -209,9 +199,25 @@ class BalancedThing(object):

thing_type = None

_account = None # underlying balanced.Account object
_customer = None # underlying balanced.Customer object
_thing = None # underlying balanced.{BankAccount,Card} object

def _get(self, name, default=""):
"""Given a name, return a unicode.
"""
out = None
if self._customer is not None and self._thing is not None:
out = self._thing
for val in name.split('.'):
if type(out) is dict:
out = out.get(val)
else:
out = getattr(out, val)
if out is None:
break
if out is None:
out = default
return out

def __init__(self, balanced_account_uri):
"""Given a Balanced account_uri, load data from Balanced.
Expand All @@ -222,10 +228,10 @@ def __init__(self, balanced_account_uri):
# XXX Indexing is borken. See:
# https://github.com/balanced/balanced-python/issues/10

self._account = balanced.Account.find(balanced_account_uri)
self._customer = balanced.Customer.fetch(balanced_account_uri)

things = getattr(self._account, self.thing_type+'s').all()
things = [thing for thing in things if thing.is_valid]
things = getattr(self._customer, self.thing_type+'s')\
.filter(is_valid=True).all()
Copy link
Contributor

Choose a reason for hiding this comment

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

in 1.1 is_valid is a default filter, no need to explicitly pass it unless you want is_valid=False

nvalid = len(things)

if nvalid == 0:
Expand All @@ -237,56 +243,38 @@ def __init__(self, balanced_account_uri):
msg %= (balanced_account_uri, len(things), self.thing_type)
raise RuntimeError(msg)

@property
def is_setup(self):
return self._thing is not None


class BalancedCard(BalancedThing):
"""This is a dict-like wrapper around a Balanced Account.
"""

thing_type = 'card'

def _get(self, name, default=""):
"""Given a name, return a unicode.
"""
out = None
if self._account is not None:
try:
out = getattr(self._thing, name, None)
except IndexError: # no cards associated
pass
if out is None:
out = default
return out

def __getitem__(self, name):
"""Given a name, return a string.
"""

if name == 'id':
out = self._account.uri if self._account is not None else None
elif name == 'last4':
out = self._get('last_four')
if out:
out = "************" + unicode(out)
elif name == 'address_2':
out = self._get('meta', {}).get('address_2', '')

elif name == 'country':
out = self._get('meta', {}).get('country', '')

elif name == 'city_town':
out = self._get('meta', {}).get('city_town', '')

elif name == 'state':
out = self._get('region')
if not out:
# There's a bug in balanced where the region does get persisted
# but doesn't make it back out. This is a workaround until such
# time as that's fixed.
out = self._get('meta', {}).get('region', '')
out = self._customer.href if self._customer is not None else None
else:
name = { 'address_1': 'street_address'
, 'zip': 'postal_code'
}.get(name, name)
name = {
'address_1': 'address.line1',
'address_2': 'meta.address_2',
'country': 'meta.country',
'city_town': 'meta.city_town',
'zip': 'address.postal_code',
# gittip is saving the state in the meta field
# for compatibility with legacy customers
'state': 'meta.region',
'last4': 'number',
'last_four': 'number',
}.get(name, name)
out = self._get(name)

return out


Expand All @@ -298,8 +286,8 @@ class BalancedBankAccount(BalancedThing):

def __getitem__(self, item):
mapper = {
'id': 'uri',
'account_uri': 'account.uri',
'id': 'href',
'customer_href': 'customer.href',
'bank_name': 'bank_name',
'last_four': 'last_four',
}
Expand All @@ -308,20 +296,4 @@ def __getitem__(self, item):
if not self._thing:
return None


# Do goofiness to support 'account.uri' in mapper.
# ================================================
# An account.uri access unrolls to:
# _item = getattr(self._thing, 'account')
# _item = getattr(_item, 'uri')

_item = self._thing
for val in mapper[item].split('.'):
_item = getattr(_item, val)


return _item

@property
def is_setup(self):
return self._thing is not None
return self._get(mapper[item])
26 changes: 13 additions & 13 deletions gittip/billing/payday.py
Expand Up @@ -580,7 +580,6 @@ def ach_credit(self, ts_start, participant, tips, total):
assert balance is not None, balance # sanity check
amount = balance - total


# Do some last-minute checks.
# ===========================

Expand Down Expand Up @@ -619,19 +618,20 @@ def ach_credit(self, ts_start, participant, tips, total):
# ===========================

try:

balanced_account_uri = participant.balanced_account_uri
if balanced_account_uri is None:
log("%s has no balanced_account_uri."
balanced_customer_href = participant.balanced_account_uri
if balanced_customer_href is None:
log("%s has no balanced_customer_href."
% participant.username)
return # not in Balanced

account = balanced.Account.find(balanced_account_uri)
if 'merchant' not in account.roles:
customer = balanced.Customer.fetch(balanced_customer_href)
if customer.merchant_status == 'underwritten':
Copy link
Contributor

Choose a reason for hiding this comment

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

balanced no longer has this constraint. only need to worry about merchant_status if > 100k/year. let's remove and simplify the code base

Copy link
Contributor

Choose a reason for hiding this comment

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

Besides, this seems to be the opposite condition as is called for. No? Wouldn't we want merchant_status != 'underwritten?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes this looks to backwards, it should be !=

Copy link
Contributor

Choose a reason for hiding this comment

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

On way or another, I've removed this in ea9d1dd.

log("%s is not a merchant." % participant.username)
return # not a merchant

account.credit(cents)
customer.bank_accounts.one()\
.credit(amount=cents,
description=participant.username)

error = ""
log(msg + "succeeded.")
Expand All @@ -642,24 +642,24 @@ def ach_credit(self, ts_start, participant, tips, total):
self.record_credit(credit_amount, fee, error, participant.username)


def charge_on_balanced(self, username, balanced_account_uri, amount):
def charge_on_balanced(self, username, balanced_customer_href, amount):
"""We have a purported balanced_account_uri. Try to use it.
"""
typecheck( username, unicode
, balanced_account_uri, unicode
, balanced_customer_href, unicode
, amount, Decimal
)

cents, msg, charge_amount, fee = self._prep_hit(amount)
msg = msg % (username, "Balanced")

try:
customer = balanced.Account.find(balanced_account_uri)
customer.debit(cents, description=username)
customer = balanced.Customer.fetch(balanced_customer_href)
customer.cards.one().debit(amount=cents, description=username)
Copy link
Contributor

Choose a reason for hiding this comment

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

.first

log(msg + "succeeded.")
error = ""
except balanced.exc.HTTPError as err:
error = err.message
error = err.message.message
log(msg + "failed: %s" % error)

return charge_amount, fee, error
Expand Down