Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
141 changes: 108 additions & 33 deletions abcbank/account.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,118 @@
from calendar import isleap
from datetime import date, timedelta
from abcbank.transaction import Transaction

CHECKING = 0
SAVINGS = 1
MAXI_SAVINGS = 2

def step(x): return x if x > 0 else 0;

class Account:
def __init__(self, accountType):
self.accountType = accountType
self.transactions = []

def deposit(self, amount):
if (amount <= 0):
raise ValueError("amount must be greater than zero")
else:
self.transactions.append(Transaction(amount))

def withdraw(self, amount):
if (amount <= 0):
raise ValueError("amount must be greater than zero")
else:
self.transactions.append(Transaction(-amount))
def savings_rate_function_generator(account, tiers=[(0, 0.001), (1000, 0.002)]):
"""
Generator for interest calculation functions for a savings account. Interest is spread evenly over the days in a year
@todo determine whether we should be breaking down the interest into twelfths and dividing those over a month

def interestEarned(self):
amount = self.sumTransactions()
if self.accountType == SAVINGS:
if (amount <= 1000):
return amount * 0.001
:param tiers: boundary values for interest rates
:type tiers: list of boundary, rate tuples, where the rate applies to values above the boundary
:return: a closure to calculate a day's interest given a transaction amount
"""
def savings_rate_function(amount, date):
tiers.sort()
balance = account.balance
interest = 0.
incremental_interest_rate = 0.
weighting = 0
for tier in tiers:
# @todo should interest be spread evenly over the days in a year
# or evenly over the months and the days in each month?
if balance > tier[0]:
incremental_interest_rate = tier[1] - incremental_interest_rate
weighting += (balance - tier[0]) * incremental_interest_rate
else:
return 1 + (amount - 1000) * 0.002
if self.accountType == MAXI_SAVINGS:
if (amount <= 1000):
return amount * 0.02
elif (amount <= 2000):
return 20 + (amount - 1000) * 0.05
else:
return 70 + (amount - 2000) * 0.1
break
effective_interest_rate = weighting/balance
days_in_year = 366. if isleap(date.year) else 365.
return amount * effective_interest_rate/days_in_year

return savings_rate_function


def checking_rate_function_generator(account, rate=0.001):
# just use the savings rate function generator with one tier
return savings_rate_function_generator(account, [(0, rate),])


def maxi_savings_rate_function_generator(account, usual_rate=0.05, for_span=timedelta(days=10), lowered_rate=0.001):
"""

:rtype: float
"""

def maxi_savings_rate_function(amount, date):
days_in_year = 366. if isleap(date.year) else 365.
# check for withdrawals trailing the interest date by less than the specified time frame
if [transaction for transaction in account._transactions if
transaction.amount < 0 and transaction.date.date() <= date and transaction.date.date() > date - for_span]:
rate = lowered_rate
else:
return amount * 0.001
rate = usual_rate
return amount * rate / days_in_year

return maxi_savings_rate_function


rate_generator_by_account_type = {
'Savings': savings_rate_function_generator
, 'Checking': checking_rate_function_generator
, 'Maxi Savings': maxi_savings_rate_function_generator
}


class Account:
"""
An Account is a list of transactions with a particular interest function generator
@todo Negative balances should probably incur some penalty
"""

def __init__(self, account_type):
if account_type not in rate_generator_by_account_type:
raise StandardError('Unkown account type {account_type}'.format(account_type=account_type))
self._transactions = []
self.account_type = account_type
self.rate_function = rate_generator_by_account_type[account_type](self)

def __str__(self):
transaction_litany = "\n".join([str(transaction) for transaction in self._transactions])
total_summary = "Total ${:1.2f}".format(sum([t.amount for t in self._transactions]))
return '''
{account_type} Account
{transaction_litany}
{total_summary}
'''.format(account_type=self.account_type, transaction_litany=transaction_litany, total_summary=total_summary)

@property
def balance(self):
return sum([transaction.amount for transaction in self._transactions])

def append_transaction(self, amount, date = None):
self._transactions.append(Transaction(amount, self.rate_function, date))

def deposit(self, amount, date = None):
if (amount <= 0): raise ValueError("amount must be greater than zero")
self.append_transaction(amount, date)

def withdraw(self, amount, date = None):
if (amount <= 0): raise ValueError("amount must be greater than zero")
self.append_transaction(-amount, date)

def interest_earned(self, end_date = None):
"""
:param end_date: fix the date so crossing midnight while iterating doesn't break us
:type end_date: datetime.date
:return: interest earned on account through the specified date
"""
return sum([transaction.interest(end_date if end_date else date.today()) for transaction in self._transactions])

def sumTransactions(self, checkAllTransactions=True):
return sum([t.amount for t in self.transactions])
@property
def statement(self):
return str(self)
42 changes: 25 additions & 17 deletions abcbank/bank.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
from datetime import date

from abcbank.customer import Customer

class Bank:
'''
A Bank is a list of Customers
'''
def __init__(self):
self.customers = []

def addCustomer(self, customer):
def add_customer(self, customer_name):
customer = Customer(customer_name)
self.customers.append(customer)
def customerSummary(self):
return customer

@property
def customer_summary(self):
summary = "Customer Summary"
for customer in self.customers:
summary = summary + "\n - " + customer.name + " (" + self._format(customer.numAccs(), "account") + ")"
summary = """{summary}
- {customer_name} ({account_count} account{s})""".format(summary = summary
, customer_name = customer.name
, account_count = len(customer.accounts)
, s = 's' if len(customer.accounts) - 1 else '')
return summary
def _format(self, number, word):
return str(number) + " " + (word if (number == 1) else word + "s")
def totalInterestPaid(self):
total = 0
for c in self.customers:
total += c.totalInterestEarned()
return total
def getFirstCustomer(self):
try:
self.customers = None
return self.customers[0].name
except Exception as e:
print(e)
return "Error"

@property
def total_interest_paid(self, end_date = None):
if end_date is None:
end_date = date.today()
return sum(sum([account.interest_earned(end_date)
for account in customer.accounts]) for customer in self.customers)
105 changes: 62 additions & 43 deletions abcbank/customer.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,74 @@
from account import CHECKING, SAVINGS, MAXI_SAVINGS
from abcbank.account import Account, rate_generator_by_account_type


class Customer:
'''
A Customer is a list of Accounts with a Name
'''

def __init__(self, name):
self.name = name
self.accounts = []
self.name = name

def openAccount(self, account):
self.accounts.append(account)
return self
def __str__(self):
statement = "Statement for %s\n" % self.name
for account in self.accounts:
statement = '%s%s' % (statement, account)
statement = '''{0:s}
Total In All Accounts ${1:1.2f}'''.format(statement
, sum([sum([transaction.amount for transaction in account._transactions])
for account in self.accounts]))
return statement

def numAccs(self):
return len(self.accounts)
def open_account(self, account_type, initial_transaction_amount=0, initial_transaction_date = None):
"""

def totalInterestEarned(self):
return sum([a.interestEarned() for a in self.accounts])
:type initial_transaction_amount: float
"""
account = Account(account_type)
self.accounts.append(account)
if initial_transaction_amount > 0:
account.deposit(initial_transaction_amount, initial_transaction_date)
elif initial_transaction_amount < 0:
account.withdraw(-initial_transaction_amount, initial_transaction_date)
return account

# This method gets a statement
def getStatement(self):
# JIRA-123 Change by Joe Bloggs 29/7/1988 start
statement = None # reset statement to null here
# JIRA-123 Change by Joe Bloggs 29/7/1988 end
totalAcrossAllAccounts = sum([a.sumTransactions() for a in self.accounts])
statement = "Statement for %s" % self.name
for account in self.accounts:
statement = statement + self.statementForAccount(account)
statement = statement + "\n\nTotal In All Accounts " + _toDollars(totalAcrossAllAccounts)
return statement
def transfer(self, from_account_type, to_account_type, amount):
'''
Go through accounts of a particular type, finding enough money to cover the transfer. Move that money to an
abitrary customer account of the specified account type, creating a new account if required

def statementForAccount(self, account):
accountType = "\n\n\n"
if account.accountType == CHECKING:
accountType = "\n\nChecking Account\n"
if account.accountType == SAVINGS:
accountType = "\n\nSavings Account\n"
if account.accountType == MAXI_SAVINGS:
accountType = "\n\nMaxi Savings Account\n"
transactionSummary = [self.withdrawalOrDepositText(t) + " " + _toDollars(abs(t.amount))
for t in account.transactions]
transactionSummary = " " + "\n ".join(transactionSummary) + "\n"
totalSummary = "Total " + _toDollars(sum([t.amount for t in account.transactions]))
return accountType + transactionSummary + totalSummary

def withdrawalOrDepositText(self, transaction):
if transaction.amount < 0:
return "withdrawal"
elif transaction.amount > 0:
return "deposit"
:param from_account_type: Account type to debit
:param to_account_type: Account type to credit
:param amount: Amount
:except: StandardError with message 'Insufficient funds: ${balance:f1.2} available; ${request:f1.2} requested'
:except: AttributeError with message 'Invalid account type {account_type}'
:return: credited account
'''
for account_type in (from_account_type, to_account_type):
if account_type not in rate_generator_by_account_type:
raise StandardError('Invalid account type {account_type}'.format(account_type=account_type))
available = sum([account.balance for account in self.accounts if account.account_type == from_account_type])
if available < amount:
raise StandardError(
'Insufficient funds: ${available:1.2f} available; ${amount:1.2f} requested'.format(
available=available, amount=amount))
remaining = amount
for account in [acct for acct in self.accounts if acct.account_type == from_account_type]:
if account.balance >= remaining:
account.withdraw(remaining)
remaining = 0
break
else:
remaining -= account.balance
account.withdraw(account.balance)
if to_account_type in [acct.account_type for acct in self.accounts]:
account = [acct for acct in self.accounts if acct.account_type == to_account_type][0]
else:
return "N/A"

account = self.open_account(to_account_type)
account.deposit(amount)
return account

def _toDollars(number):
return "${:1.2f}".format(number)
@property
def statement(self):
return str(self)
7 changes: 0 additions & 7 deletions abcbank/date_provider.py

This file was deleted.

53 changes: 49 additions & 4 deletions abcbank/transaction.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,52 @@
from datetime import datetime
from datetime import datetime, date, timedelta


class Transaction:
def __init__(self, amount):
class Transaction(object):
"""
A Transaction is an amount of money at a time that accrues according to a closure
"""

def __init__(self, amount, interest_function = lambda y, z: 0, date=None):
"""

:param amount: Value of transaction -- positive for deposits, negative for withdrawals
:param interest_function: function that returns an amount of interest given a balance and a date. Defaults to returning 0
:type interest_function: function
:param date: effective date of transaction from which interest is calculated
:type date: datetime.datetime
:return:
"""
self.amount = amount
self.transactionDate = datetime.now()
self.date = date if date else datetime.now()
self.interest_function = interest_function

def __str__(self):
return " {withdrawal_or_deposit} {amount}".format(
withdrawal_or_deposit='withdrawal' if self.amount < 0 else 'deposit'
, amount="${:1.2f}".format(abs(self.amount)))


def __repr__(self):
return '{str} at {date}'.format(str=str(self).strip(), date=self.date)


def interest(self, end_date = None):
"""
Calculates the transaction's compound interest by applying the account's interest function to this transaction
from the transaction date to the prior date (ie, not today)

:param end_date: date through which to calculate interest
:type end_date: datetime.date
:return: interest payable due to this transaction
"""
balance = self.amount
# range give an empty list for any negative number, so we don't have to compare end_date to self.date
'''
This was an unexpected little Python 3 change
'end_date if end_date else date.today() - self.date.date()' from my Python 2.7 code now gets interpreted as
'end_date if end_date else (date.today() - self.date.date())', which of course breaks this. So, now I have
'(end_date if end_date else date.today()) - self.date.date()'
'''
for delta in range(((end_date if end_date else date.today()) - self.date.date()).days):
balance += self.interest_function(balance, self.date.date() + timedelta(days = delta))
return balance - self.amount
Loading