diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abcbank/account.py b/abcbank/account.py index e010009..6d61be8 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -1,4 +1,5 @@ from abcbank.transaction import Transaction +from datetime import datetime CHECKING = 0 SAVINGS = 1 @@ -6,38 +7,75 @@ class Account: + def __init__(self, accountType): self.accountType = accountType self.transactions = [] + self.amount = 0. - def deposit(self, amount): + def deposit(self, amount, t=None): + t = datetime.now() if t is None else t if (amount <= 0): raise ValueError("amount must be greater than zero") else: - self.transactions.append(Transaction(amount)) + self.transactions.append(Transaction(amount, t)) + self.amount += amount - def withdraw(self, amount): + def withdraw(self, amount, t=None): + t = datetime.now() if t is None else t if (amount <= 0): raise ValueError("amount must be greater than zero") else: - self.transactions.append(Transaction(-amount)) + self.transactions.append(Transaction(-amount, t)) + self.amount -= amount def interestEarned(self): - amount = self.sumTransactions() + if len(self.transactions) == 0: + return 0. + if len(self.transactions) == 1 and not self.transactions[0].withdraw: + t = self.transactions[0] + return self.interest(t.amount, t.age) + interest = 0. + last_amount = 0. + sorted_transactions = sorted(self.transactions, + key = lambda x: x.transactionDate) + for i, t in enumerate(sorted_transactions): + last_amount += t.amount + if i == 0: + continue + wd = self.withdrawalsInLastNDays(sorted_transactions[:i+1]) + days_since_last_transaction = sorted_transactions[i-1].age - t.age + interest += self.interest(last_amount - t.amount, + days_since_last_transaction, wd) + wd = self.withdrawalsInLastNDays(sorted_transactions, current=True) + interest += self.interest(last_amount, t.age, wd) + return interest + + def interest(self, amount, days, withdrawals=False): if self.accountType == SAVINGS: if (amount <= 1000): - return amount * 0.001 + result = amount * 0.001 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 + result = 1 + (amount - 1000) * 0.002 + elif self.accountType == MAXI_SAVINGS: + if withdrawals: + result = amount * 0.001 else: - return 70 + (amount - 2000) * 0.1 + result = amount * 0.05 else: - return amount * 0.001 + result = amount * 0.001 + return result * float(days)/365 + + def withdrawalsInLastNDays(self, transactions, N=10, current=False): + last = 0 if current else transactions[-1].age + for i, t in enumerate(reversed(transactions)): + if not current and i == 0: + continue + if t.age - last > N: + return False + elif t.withdraw: + return True + return False def sumTransactions(self, checkAllTransactions=True): - return sum([t.amount for t in self.transactions]) \ No newline at end of file + return sum([t.amount for t in self.transactions]) diff --git a/abcbank/bank.py b/abcbank/bank.py index 44711fe..7bb9059 100644 --- a/abcbank/bank.py +++ b/abcbank/bank.py @@ -1,25 +1,31 @@ class Bank: + def __init__(self): self.customers = [] def addCustomer(self, customer): self.customers.append(customer) + def customerSummary(self): summary = "Customer Summary" for customer in self.customers: - summary = summary + "\n - " + customer.name + " (" + self._format(customer.numAccs(), "account") + ")" + summary = summary + "\n - " + customer.name +\ + " (" + self._format(customer.numAccs(), "account") + ")" 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" \ No newline at end of file + return "Error" diff --git a/abcbank/customer.py b/abcbank/customer.py index 7cfd62a..cba6c38 100644 --- a/abcbank/customer.py +++ b/abcbank/customer.py @@ -2,6 +2,7 @@ class Customer: + def __init__(self, name): self.name = name self.accounts = [] @@ -21,11 +22,13 @@ 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]) + 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) + statement = statement + "\n\nTotal In All Accounts " +\ + _toDollars(totalAcrossAllAccounts) return statement def statementForAccount(self, account): @@ -36,10 +39,12 @@ def statementForAccount(self, account): accountType = "\n\nSavings Account\n" if account.accountType == MAXI_SAVINGS: accountType = "\n\nMaxi Savings Account\n" - transactionSummary = [self.withdrawalOrDepositText(t) + " " + _toDollars(abs(t.amount)) + 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])) + totalSummary = "Total " + _toDollars(sum([t.amount for t in + account.transactions])) return accountType + transactionSummary + totalSummary def withdrawalOrDepositText(self, transaction): @@ -50,6 +55,20 @@ def withdrawalOrDepositText(self, transaction): else: return "N/A" + def transfer(self, from_account, to_account, amount): + if amount <= 0: + raise ValueError('Incorrect amount, should be more than 0') + if from_account.amount < amount: + raise ValueError('Incorrect amount, should be more than ' + 'source account amount') + from_account.withdraw(amount) + try: + to_account.deposit(amount) + except: + from_account.deposit(amount) + raise + return True + def _toDollars(number): return "${:1.2f}".format(number) diff --git a/abcbank/date_provider.py b/abcbank/date_provider.py index 33b64eb..6b0c860 100644 --- a/abcbank/date_provider.py +++ b/abcbank/date_provider.py @@ -2,6 +2,7 @@ class DateProvider: + @staticmethod def now(): - return datetime.now() \ No newline at end of file + return datetime.now() diff --git a/abcbank/transaction.py b/abcbank/transaction.py index 8e5b5ad..b1351ca 100644 --- a/abcbank/transaction.py +++ b/abcbank/transaction.py @@ -2,6 +2,13 @@ class Transaction: - def __init__(self, amount): + + def __init__(self, amount, transaction_time=None): self.amount = amount - self.transactionDate = datetime.now() \ No newline at end of file + self.transactionDate = datetime.now() if transaction_time is None\ + else transaction_time + self.withdraw = True if amount < 0 else False + + @property + def age(self): + return (datetime.now() - self.transactionDate).days diff --git a/test.py b/test.py new file mode 100644 index 0000000..2f93a21 --- /dev/null +++ b/test.py @@ -0,0 +1,23 @@ +from tests import bank_tests, customer_tests, transaction_tests + + +def run(): + print 'Running bank tests...' + b_tests = [getattr(bank_tests, obj) for obj in dir(bank_tests) + if 'test_' in obj] + for func in b_tests: + func() + print 'Running customer tests...' + c_tests = [getattr(customer_tests, obj) for obj in dir(customer_tests) + if 'test_' in obj] + for func in c_tests: + func() + print 'Running transactions tests...' + t_tests = [getattr(transaction_tests, obj) + for obj in dir(transaction_tests) if 'test_' in obj] + for func in t_tests: + func() + print 'Done' + +if __name__ == '__main__': + run() diff --git a/tests/bank_tests.py b/tests/bank_tests.py index 6de98db..43ab2bc 100644 --- a/tests/bank_tests.py +++ b/tests/bank_tests.py @@ -1,8 +1,9 @@ -from nose.tools import assert_equals +from nose.tools import assert_equals, assert_almost_equals +import datetime -from account import Account, CHECKING, MAXI_SAVINGS, SAVINGS -from bank import Bank -from customer import Customer +from abcbank.account import Account, CHECKING, MAXI_SAVINGS, SAVINGS +from abcbank.bank import Bank +from abcbank.customer import Customer def test_customer_summary(): @@ -18,7 +19,8 @@ def test_checking_account(): checkingAccount = Account(CHECKING) bill = Customer("Bill").openAccount(checkingAccount) bank.addCustomer(bill) - checkingAccount.deposit(100.0) + year_behind = datetime.datetime.now() - datetime.timedelta(365) + checkingAccount.deposit(100.0, year_behind) assert_equals(bank.totalInterestPaid(), 0.1) @@ -26,7 +28,8 @@ def test_savings_account(): bank = Bank() checkingAccount = Account(SAVINGS) bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) - checkingAccount.deposit(1500.0) + year_behind = datetime.datetime.now() - datetime.timedelta(365) + checkingAccount.deposit(1500.0, year_behind) assert_equals(bank.totalInterestPaid(), 2.0) @@ -34,5 +37,28 @@ def test_maxi_savings_account(): bank = Bank() checkingAccount = Account(MAXI_SAVINGS) bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) - checkingAccount.deposit(3000.0) - assert_equals(bank.totalInterestPaid(), 170.0) \ No newline at end of file + year_behind = datetime.datetime.now() - datetime.timedelta(365) + checkingAccount.deposit(3000.0, year_behind) + assert_equals(bank.totalInterestPaid(), 150.0) + + +def test_maxi_savings_account_without_withdrawals(): + bank = Bank() + checkingAccount = Account(MAXI_SAVINGS) + bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) + year_behind = datetime.datetime.now() - datetime.timedelta(365) + eleven_days_behind = datetime.datetime.now() - datetime.timedelta(11) + checkingAccount.deposit(3000.0, year_behind) + checkingAccount.withdraw(1000.0, eleven_days_behind) + assert_almost_equals(bank.totalInterestPaid(), 148.49, places=2) + + +def test_maxi_savings_account_with_withdrawals(): + bank = Bank() + checkingAccount = Account(MAXI_SAVINGS) + bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) + year_behind = datetime.datetime.now() - datetime.timedelta(365) + five_days_behind = datetime.datetime.now() - datetime.timedelta(5) + checkingAccount.deposit(3000.0, year_behind) + checkingAccount.withdraw(1000.0, five_days_behind) + assert_almost_equals(bank.totalInterestPaid(), 147.97, places=2) diff --git a/tests/customer_tests.py b/tests/customer_tests.py index 0211a4f..1aa32f1 100644 --- a/tests/customer_tests.py +++ b/tests/customer_tests.py @@ -1,20 +1,23 @@ -from nose.tools import assert_equals, nottest +from nose.tools import assert_equals, assert_almost_equals +from datetime import datetime -from account import Account, CHECKING, SAVINGS -from customer import Customer +from abcbank.account import Account, CHECKING, SAVINGS, MAXI_SAVINGS +from abcbank.customer import Customer def test_statement(): checkingAccount = Account(CHECKING) savingsAccount = Account(SAVINGS) - henry = Customer("Henry").openAccount(checkingAccount).openAccount(savingsAccount) + henry = Customer("Henry").openAccount(checkingAccount).\ + openAccount(savingsAccount) checkingAccount.deposit(100.0) savingsAccount.deposit(4000.0) savingsAccount.withdraw(200.0) assert_equals(henry.getStatement(), "Statement for Henry" + "\n\nChecking Account\n deposit $100.00\nTotal $100.00" + - "\n\nSavings Account\n deposit $4000.00\n withdrawal $200.00\nTotal $3800.00" + + "\n\nSavings Account\n deposit $4000.00\n " + "withdrawal $200.00\nTotal $3800.00" + "\n\nTotal In All Accounts $3900.00") @@ -29,8 +32,55 @@ def test_twoAccounts(): assert_equals(oscar.numAccs(), 2) -@nottest def test_threeAccounts(): oscar = Customer("Oscar").openAccount(Account(SAVINGS)) oscar.openAccount(Account(CHECKING)) - assert_equals(oscar.numAccs(), 3) \ No newline at end of file + oscar.openAccount(Account(MAXI_SAVINGS)) + assert_equals(oscar.numAccs(), 3) + + +def test_withdrawals(): + checkingAccount = Account(CHECKING) + oscar = Customer("Oscar").openAccount(checkingAccount) + checkingAccount.deposit(1000.0, datetime(2016, 4, 12)) + checkingAccount.deposit(1000.0, datetime(2016, 4, 15)) + checkingAccount.deposit(1000.0, datetime(2016, 4, 17)) + assert_almost_equals(oscar.totalInterestEarned(), 0.17, places=2) + checkingAccount.withdraw(1000.0, datetime(2016, 4, 19)) + assert_almost_equals(oscar.totalInterestEarned(), 0.12, places=2) + + +def test_transferAmountBelowZero(): + checkingAccount = Account(CHECKING) + savingsAccount = Account(SAVINGS) + oscar = Customer("Oscar").openAccount(checkingAccount) + oscar.openAccount(savingsAccount) + try: + oscar.transfer(checkingAccount, savingsAccount, -1) + except ValueError as e: + result = str(e) + assert_equals(result, 'Incorrect amount, should be more than 0') + + +def test_transferNotEnoughFunds(): + checkingAccount = Account(CHECKING) + savingsAccount = Account(SAVINGS) + oscar = Customer("Oscar").openAccount(checkingAccount) + oscar.openAccount(savingsAccount) + try: + oscar.transfer(savingsAccount, savingsAccount, 10) + except ValueError as e: + result = str(e) + assert_equals(result, 'Incorrect amount, should be more than ' + 'source account amount') + + +def test_transferAmount(): + checkingAccount = Account(CHECKING) + savingsAccount = Account(SAVINGS) + oscar = Customer("Oscar").openAccount(checkingAccount) + oscar.openAccount(savingsAccount) + checkingAccount.deposit(100.0) + transfered = oscar.transfer(checkingAccount, savingsAccount, 10) + assert_equals(transfered, True) + assert_equals(savingsAccount.amount, 10.) diff --git a/tests/transaction_tests.py b/tests/transaction_tests.py index 62caa8a..5bce7fc 100644 --- a/tests/transaction_tests.py +++ b/tests/transaction_tests.py @@ -1,8 +1,14 @@ -from nose.tools import assert_is_instance +from nose.tools import assert_is_instance, assert_true, assert_false -from transaction import Transaction +from abcbank.transaction import Transaction def test_type(): t = Transaction(5) - assert_is_instance(t, Transaction, "correct type") \ No newline at end of file + assert_is_instance(t, Transaction, "correct type") + assert_false(t.withdraw) + + +def test_withdraw(): + t = Transaction(-5) + assert_true(t.withdraw)