Skip to content

Commit

Permalink
issue #104 - WIP calculate minimum payments for all credit accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
jantman committed Aug 13, 2017
1 parent ec68a93 commit 7e56436
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 11 deletions.
53 changes: 52 additions & 1 deletion biweeklybudget/interest.py
Expand Up @@ -55,6 +55,7 @@ def __init__(self, db_sess):
"""
self._sess = db_sess
self._accounts = self._get_credit_accounts()
self._statements = self._make_statements(self._accounts)

@property
def accounts(self):
Expand Down Expand Up @@ -82,6 +83,55 @@ def _get_credit_accounts(self):
res = {a.id: a for a in accts}
return res

def _make_statements(self, accounts):
"""
Make :py:class:`~.CCStatement` instances for each account; return a
dict of `account_id` to CCStatement instance.
:param accounts: dict of (int) account_id to Account instance
:type accounts: dict
:return: dict of (int) account_id to CCStatement instance
:rtype: dict
"""
res = {}
for a_id, acct in accounts.items():
icharge = acct.latest_ofx_interest_charge
istmt = icharge.first_statement_by_date
icls = INTEREST_CALCULATION_NAMES[acct.interest_class_name]['cls'](
acct.apr
)
bpa = acct.billing_period_class_args_deserialized
bpargs = bpa.get('args', [])
bpargs.insert(0, icharge.date_posted.date())
bpkwargs = bpa.get('kwargs', {})
bill_period = BILLING_PERIOD_NAMES[
acct.billing_period_class_name]['cls'](*bpargs, **bpkwargs)
min_pay_cls = MIN_PAYMENT_FORMULA_NAMES[
acct.min_payment_class_name]['cls']()
res[a_id] = CCStatement(
icls,
abs(istmt.ledger_bal),
min_pay_cls,
bill_period,
end_balance=abs(istmt.ledger_bal),
interest_amt=abs(icharge.account_amount)
)
return res

@property
def min_payments(self):
"""
Return a dict of `account_id` to minimum payment for the latest
statement, for each account.
:return: dict of `account_id` to minimum payment (Decimal)
:rtype: dict
"""
res = {}
for a_id, stmt in self._statements.items():
res[a_id] = stmt.minimum_payment
return res


class _InterestCalculation(object):

Expand Down Expand Up @@ -697,7 +747,8 @@ def subclass_dict(klass):
for cls in klass.__subclasses__():
d[cls.__name__] = {
'description': cls.description,
'doc': cls.__doc__.strip()
'doc': cls.__doc__.strip(),
'cls': cls
}
return d

Expand Down
49 changes: 47 additions & 2 deletions biweeklybudget/tests/acceptance/test_interest.py
Expand Up @@ -36,16 +36,21 @@
"""

import pytest
from datetime import date
from decimal import Decimal

from biweeklybudget.tests.acceptance_helpers import AcceptanceHelper
from biweeklybudget.interest import InterestHelper
from biweeklybudget.interest import (
InterestHelper, CCStatement, BillingPeriodNumDays, AdbCompoundedDaily,
MinPaymentAmEx, MinPaymentDiscover
)
from biweeklybudget.models import Account


@pytest.mark.acceptance
class TestInterestHelper(AcceptanceHelper):

def test_init(self, testdb):
def test_init_accounts(self, testdb):
cls = InterestHelper(testdb)
assert cls._sess == testdb
res = cls.accounts
Expand All @@ -54,3 +59,43 @@ def test_init(self, testdb):
assert res[3].id == 3
assert isinstance(res[4], Account)
assert res[4].id == 4

def test_init_statements(self, testdb):
cls = InterestHelper(testdb)
res = cls._statements
assert sorted(res.keys()) == [3, 4]
s3 = res[3]
assert isinstance(s3, CCStatement)
assert isinstance(s3._billing_period, BillingPeriodNumDays)
assert s3._billing_period.num_days == 30
assert s3._billing_period._end_date == date(2017, 7, 27)
assert s3._billing_period._start_date == date(2017, 6, 27)
assert isinstance(s3._interest_cls, AdbCompoundedDaily)
assert s3._interest_cls.apr == Decimal('0.0100')
assert isinstance(s3._min_pay_cls, MinPaymentAmEx)
assert s3._orig_principal == Decimal('952.06')
assert s3._min_pay is None
assert s3._transactions == {}
assert s3._principal == Decimal('952.06')
assert s3._interest_amt == Decimal('16.25')
s4 = res[4]
assert isinstance(s4, CCStatement)
assert isinstance(s4._billing_period, BillingPeriodNumDays)
assert s4._billing_period.num_days == 30
assert s4._billing_period._end_date == date(2017, 7, 26)
assert s4._billing_period._start_date == date(2017, 6, 26)
assert isinstance(s4._interest_cls, AdbCompoundedDaily)
assert s4._interest_cls.apr == Decimal('0.1000')
assert isinstance(s4._min_pay_cls, MinPaymentDiscover)
assert s4._orig_principal == Decimal('5498.65')
assert s4._min_pay is None
assert s4._transactions == {}
assert s4._principal == Decimal('5498.65')
assert s4._interest_amt == Decimal('28.53')

def test_min_payments(self, testdb):
cls = InterestHelper(testdb)
assert cls.min_payments == {
3: Decimal('35'),
4: Decimal('109.9730')
}
24 changes: 16 additions & 8 deletions biweeklybudget/tests/unit/test_interest.py
Expand Up @@ -655,47 +655,55 @@ def test_interest(self):
assert INTEREST_CALCULATION_NAMES == {
'AdbCompoundedDaily': {
'description': AdbCompoundedDaily.description,
'doc': AdbCompoundedDaily.__doc__.strip()
'doc': AdbCompoundedDaily.__doc__.strip(),
'cls': AdbCompoundedDaily
}
}

def test_billing(self):
assert BILLING_PERIOD_NAMES == {
'BillingPeriodNumDays': {
'description': BillingPeriodNumDays.description,
'doc': BillingPeriodNumDays.__doc__.strip()
'doc': BillingPeriodNumDays.__doc__.strip(),
'cls': BillingPeriodNumDays
}
}

def test_min_payment(self):
assert MIN_PAYMENT_FORMULA_NAMES == {
'MinPaymentAmEx': {
'description': MinPaymentAmEx.description,
'doc': MinPaymentAmEx.__doc__.strip()
'doc': MinPaymentAmEx.__doc__.strip(),
'cls': MinPaymentAmEx
},
'MinPaymentDiscover': {
'description': MinPaymentDiscover.description,
'doc': MinPaymentDiscover.__doc__.strip()
'doc': MinPaymentDiscover.__doc__.strip(),
'cls': MinPaymentDiscover
},
'MinPaymentCiti': {
'description': MinPaymentCiti.description,
'doc': MinPaymentCiti.__doc__.strip()
'doc': MinPaymentCiti.__doc__.strip(),
'cls': MinPaymentCiti
}
}

def test_payoff(self):
assert PAYOFF_METHOD_NAMES == {
'MinPaymentMethod': {
'description': MinPaymentMethod.description,
'doc': MinPaymentMethod.__doc__.strip()
'doc': MinPaymentMethod.__doc__.strip(),
'cls': MinPaymentMethod
},
'FixedPaymentMethod': {
'description': FixedPaymentMethod.description,
'doc': FixedPaymentMethod.__doc__.strip()
'doc': FixedPaymentMethod.__doc__.strip(),
'cls': FixedPaymentMethod
},
'LowestBalanceFirstMethod': {
'description': LowestBalanceFirstMethod.description,
'doc': LowestBalanceFirstMethod.__doc__.strip()
'doc': LowestBalanceFirstMethod.__doc__.strip(),
'cls': LowestBalanceFirstMethod
}
}

Expand Down

0 comments on commit 7e56436

Please sign in to comment.