From 51caccaea2e47923a125945af60e30eba612906d Mon Sep 17 00:00:00 2001 From: "CentOS 6.5" Date: Thu, 10 Dec 2015 08:39:11 -0800 Subject: [PATCH 01/11] Fully qualify the package name in import statements. This is necessary to make the unit test "./tests/" directory find them. --- abcbank/customer.py | 2 +- tests/bank_tests.py | 8 ++++---- tests/customer_tests.py | 6 +++--- tests/transaction_tests.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/abcbank/customer.py b/abcbank/customer.py index 7cfd62a..d953052 100644 --- a/abcbank/customer.py +++ b/abcbank/customer.py @@ -1,4 +1,4 @@ -from account import CHECKING, SAVINGS, MAXI_SAVINGS +from abcbank.account import CHECKING, SAVINGS, MAXI_SAVINGS class Customer: diff --git a/tests/bank_tests.py b/tests/bank_tests.py index 6de98db..b853ead 100644 --- a/tests/bank_tests.py +++ b/tests/bank_tests.py @@ -1,8 +1,8 @@ from nose.tools import assert_equals -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(): @@ -35,4 +35,4 @@ def test_maxi_savings_account(): 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 + assert_equals(bank.totalInterestPaid(), 170.0) diff --git a/tests/customer_tests.py b/tests/customer_tests.py index 0211a4f..c46011a 100644 --- a/tests/customer_tests.py +++ b/tests/customer_tests.py @@ -1,7 +1,7 @@ from nose.tools import assert_equals, nottest -from account import Account, CHECKING, SAVINGS -from customer import Customer +from abcbank.account import Account, CHECKING, SAVINGS +from abcbank.customer import Customer def test_statement(): @@ -33,4 +33,4 @@ def test_twoAccounts(): def test_threeAccounts(): oscar = Customer("Oscar").openAccount(Account(SAVINGS)) oscar.openAccount(Account(CHECKING)) - assert_equals(oscar.numAccs(), 3) \ No newline at end of file + assert_equals(oscar.numAccs(), 3) diff --git a/tests/transaction_tests.py b/tests/transaction_tests.py index 62caa8a..5227d39 100644 --- a/tests/transaction_tests.py +++ b/tests/transaction_tests.py @@ -1,8 +1,8 @@ from nose.tools import assert_is_instance -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") From 2acda974b23731d071fddbe215177a3a372658fe Mon Sep 17 00:00:00 2001 From: adroffner Date: Thu, 10 Dec 2015 09:22:56 -0800 Subject: [PATCH 02/11] Convert ./tests/ directory to standard Python unittest TestCases. This will make it easier to improve test coverage with exception handling cases. --- tests/bank_tests.py | 67 +++++++++++++++++++------------------- tests/customer_tests.py | 65 ++++++++++++++++++------------------ tests/transaction_tests.py | 11 ++++--- 3 files changed, 73 insertions(+), 70 deletions(-) diff --git a/tests/bank_tests.py b/tests/bank_tests.py index b853ead..f73e316 100644 --- a/tests/bank_tests.py +++ b/tests/bank_tests.py @@ -1,38 +1,39 @@ -from nose.tools import assert_equals +from unittest import TestCase from abcbank.account import Account, CHECKING, MAXI_SAVINGS, SAVINGS from abcbank.bank import Bank from abcbank.customer import Customer - -def test_customer_summary(): - bank = Bank() - john = Customer("John").openAccount(Account(CHECKING)) - bank.addCustomer(john) - assert_equals(bank.customerSummary(), - "Customer Summary\n - John (1 account)") - - -def test_checking_account(): - bank = Bank() - checkingAccount = Account(CHECKING) - bill = Customer("Bill").openAccount(checkingAccount) - bank.addCustomer(bill) - checkingAccount.deposit(100.0) - assert_equals(bank.totalInterestPaid(), 0.1) - - -def test_savings_account(): - bank = Bank() - checkingAccount = Account(SAVINGS) - bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) - checkingAccount.deposit(1500.0) - assert_equals(bank.totalInterestPaid(), 2.0) - - -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) +class CustomerTests(TestCase): + + def test_customer_summary(self): + bank = Bank() + john = Customer("John").openAccount(Account(CHECKING)) + bank.addCustomer(john) + self.assertEquals(bank.customerSummary(), + "Customer Summary\n - John (1 account)") + + + def test_checking_account(self): + bank = Bank() + checkingAccount = Account(CHECKING) + bill = Customer("Bill").openAccount(checkingAccount) + bank.addCustomer(bill) + checkingAccount.deposit(100.0) + self.assertEquals(bank.totalInterestPaid(), 0.1) + + + def test_savings_account(self): + bank = Bank() + checkingAccount = Account(SAVINGS) + bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) + checkingAccount.deposit(1500.0) + self.assertEquals(bank.totalInterestPaid(), 2.0) + + + def test_maxi_savings_account(self): + bank = Bank() + checkingAccount = Account(MAXI_SAVINGS) + bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) + checkingAccount.deposit(3000.0) + self.assertEquals(bank.totalInterestPaid(), 170.0) diff --git a/tests/customer_tests.py b/tests/customer_tests.py index c46011a..da8032e 100644 --- a/tests/customer_tests.py +++ b/tests/customer_tests.py @@ -1,36 +1,37 @@ -from nose.tools import assert_equals, nottest +from unittest import TestCase, skip from abcbank.account import Account, CHECKING, SAVINGS from abcbank.customer import Customer - -def test_statement(): - checkingAccount = Account(CHECKING) - savingsAccount = Account(SAVINGS) - 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\nTotal In All Accounts $3900.00") - - -def test_oneAccount(): - oscar = Customer("Oscar").openAccount(Account(SAVINGS)) - assert_equals(oscar.numAccs(), 1) - - -def test_twoAccounts(): - oscar = Customer("Oscar").openAccount(Account(SAVINGS)) - oscar.openAccount(Account(CHECKING)) - assert_equals(oscar.numAccs(), 2) - - -@nottest -def test_threeAccounts(): - oscar = Customer("Oscar").openAccount(Account(SAVINGS)) - oscar.openAccount(Account(CHECKING)) - assert_equals(oscar.numAccs(), 3) +class StatementTests(TestCase): + + def test_statement(self): + checkingAccount = Account(CHECKING) + savingsAccount = Account(SAVINGS) + henry = Customer("Henry").openAccount(checkingAccount).openAccount(savingsAccount) + checkingAccount.deposit(100.0) + savingsAccount.deposit(4000.0) + savingsAccount.withdraw(200.0) + self.assertEquals(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\nTotal In All Accounts $3900.00") + + + def test_oneAccount(self): + oscar = Customer("Oscar").openAccount(Account(SAVINGS)) + self.assertEquals(oscar.numAccs(), 1) + + + def test_twoAccounts(self): + oscar = Customer("Oscar").openAccount(Account(SAVINGS)) + oscar.openAccount(Account(CHECKING)) + self.assertEquals(oscar.numAccs(), 2) + + + @skip + def test_threeAccounts(self): + oscar = Customer("Oscar").openAccount(Account(SAVINGS)) + oscar.openAccount(Account(CHECKING)) + self.assertEquals(oscar.numAccs(), 3) diff --git a/tests/transaction_tests.py b/tests/transaction_tests.py index 5227d39..959904a 100644 --- a/tests/transaction_tests.py +++ b/tests/transaction_tests.py @@ -1,8 +1,9 @@ -from nose.tools import assert_is_instance +from unittest import TestCase from abcbank.transaction import Transaction - -def test_type(): - t = Transaction(5) - assert_is_instance(t, Transaction, "correct type") +class TransactionTests(TestCase): + + def test_type(self): + t = Transaction(5) + self.assertIsInstance(t, Transaction, "correct type") From 722cfedcc88a11271c22978ed3142242ca49e5f0 Mon Sep 17 00:00:00 2001 From: adroffner Date: Thu, 10 Dec 2015 09:54:43 -0800 Subject: [PATCH 03/11] Add tests/account_tests.py module to test account actions, including exception handling. --- abcbank/account.py | 6 +++--- tests/account_tests.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 tests/account_tests.py diff --git a/abcbank/account.py b/abcbank/account.py index e010009..99e8d9c 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -12,13 +12,13 @@ def __init__(self, accountType): def deposit(self, amount): if (amount <= 0): - raise ValueError("amount must be greater than zero") + raise ValueError("deposit 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") + raise ValueError("withdraw amount must be greater than zero") else: self.transactions.append(Transaction(-amount)) @@ -40,4 +40,4 @@ def interestEarned(self): return amount * 0.001 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/tests/account_tests.py b/tests/account_tests.py new file mode 100644 index 0000000..955a889 --- /dev/null +++ b/tests/account_tests.py @@ -0,0 +1,34 @@ +from unittest import TestCase + +from abcbank.transaction import Transaction +from abcbank.account import Account, CHECKING, SAVINGS, MAXI_SAVINGS + + +class AccountTests(TestCase): + + def test_deposit(self): + amount = 5 + a = Account(CHECKING) + a.deposit(amount) + self.assertGreaterEqual(amount, 0) + self.assertIsInstance(a.transactions[0], Transaction, "correct type") + + def test_deposit_error(self): + amount = -5 + a = Account(CHECKING) + with self.assertRaisesRegexp(ValueError, r'deposit amount must be greater than zero'): + a.deposit(amount) + + def test_withdraw(self): + amount = 2 + a = Account(CHECKING) + a.withdraw(amount) + self.assertLessEqual(amount, 0) + self.assertIsInstance(a.transactions[0], Transaction, "correct type") + + def test_withdraw_error(self): + amount = -2 + a = Account(CHECKING) + with self.assertRaisesRegexp(ValueError, r'withdraw amount must be greater than zero'): + a.withdraw(amount) + From e5fed1695fabc88167eedd163cfd89770372646e Mon Sep 17 00:00:00 2001 From: adroffner Date: Thu, 10 Dec 2015 20:36:51 -0800 Subject: [PATCH 04/11] Both deposit(amount) and withdraw(amount) expect positive-valued input. The unit tests match this rule now. --- tests/account_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/account_tests.py b/tests/account_tests.py index 955a889..d66842c 100644 --- a/tests/account_tests.py +++ b/tests/account_tests.py @@ -23,7 +23,7 @@ def test_withdraw(self): amount = 2 a = Account(CHECKING) a.withdraw(amount) - self.assertLessEqual(amount, 0) + self.assertGreaterEqual(amount, 0) self.assertIsInstance(a.transactions[0], Transaction, "correct type") def test_withdraw_error(self): From 14c7020a4c1fe51e623df12b141746dec89c54ce Mon Sep 17 00:00:00 2001 From: adroffner Date: Thu, 10 Dec 2015 21:44:39 -0800 Subject: [PATCH 05/11] Add help docs to account.py and give its unit tests 100% code coverage. --- abcbank/account.py | 35 +++++++++++++++++++++++--- tests/account_tests.py | 56 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/abcbank/account.py b/abcbank/account.py index 99e8d9c..225609d 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -5,39 +5,68 @@ MAXI_SAVINGS = 2 -class Account: +class Account(object): + ''' An Account belongs to a Customer. + ''' + def __init__(self, accountType): + '''Create an Account. + + :param int accountType: an account type, e.g. account.CHECKING + ''' self.accountType = accountType self.transactions = [] def deposit(self, amount): + ''' Deposit `amount` dollars into this account. + The `amount` is a positive dollar value taken from the customer. + + :param float amount: deposit amount in USD + ''' if (amount <= 0): raise ValueError("deposit amount must be greater than zero") else: self.transactions.append(Transaction(amount)) def withdraw(self, amount): + ''' Withdraw `amount` dollars from this account. + The `amount` is a positive dollar value given to the customer. + + :param float amount: withdraw amount in USD + ''' if (amount <= 0): raise ValueError("withdraw amount must be greater than zero") else: self.transactions.append(Transaction(-amount)) def interestEarned(self): + ''' Calculate the interest earned on all transactions. + This computes interest on all deposits and withdrawls during the period. + + :returns: interest earned in USD + ''' amount = self.sumTransactions() if self.accountType == SAVINGS: if (amount <= 1000): return amount * 0.001 else: return 1 + (amount - 1000) * 0.002 - if self.accountType == MAXI_SAVINGS: + elif 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 - else: + elif self.accountType == CHECKING: return amount * 0.001 + else: + raise ValueError("account has an invalid account type") def sumTransactions(self, checkAllTransactions=True): + ''' Computes all deposits and withdrawls during the period. + + :returns: current monies without interest in USD + ''' return sum([t.amount for t in self.transactions]) + diff --git a/tests/account_tests.py b/tests/account_tests.py index d66842c..d417b35 100644 --- a/tests/account_tests.py +++ b/tests/account_tests.py @@ -7,28 +7,76 @@ class AccountTests(TestCase): def test_deposit(self): - amount = 5 + amount = 5.0 a = Account(CHECKING) a.deposit(amount) self.assertGreaterEqual(amount, 0) self.assertIsInstance(a.transactions[0], Transaction, "correct type") def test_deposit_error(self): - amount = -5 + amount = -5.0 a = Account(CHECKING) with self.assertRaisesRegexp(ValueError, r'deposit amount must be greater than zero'): a.deposit(amount) def test_withdraw(self): - amount = 2 + amount = 2.0 a = Account(CHECKING) a.withdraw(amount) self.assertGreaterEqual(amount, 0) self.assertIsInstance(a.transactions[0], Transaction, "correct type") def test_withdraw_error(self): - amount = -2 + amount = -2.0 a = Account(CHECKING) with self.assertRaisesRegexp(ValueError, r'withdraw amount must be greater than zero'): a.withdraw(amount) + def test_interest_checking(self): + amount = 200.00 + interest = amount * 0.001 + a = Account(CHECKING) + a.deposit(amount) + iy = a.interestEarned() + self.assertEqual(iy, interest) + + def test_interest_savings_low(self): + amount = 370.00 + interest = amount * 0.001 + a = Account(SAVINGS) + a.deposit(amount) + iy = a.interestEarned() + self.assertEqual(iy, interest) + + def test_interest_savings_high(self): + amount = 1250.00 + interest = (amount - 1000.0) * 0.002 + 1.0 + a = Account(SAVINGS) + a.deposit(amount) + iy = a.interestEarned() + self.assertEqual(iy, interest) + + def test_interest_maxi_savings_low(self): + amount = 370.00 + interest = amount * 0.02 + a = Account(MAXI_SAVINGS) + a.deposit(amount) + iy = a.interestEarned() + self.assertEqual(iy, interest) + + def test_interest_maxi_savings_high(self): + amount = 1250.00 + interest = (amount - 1000) * 0.05 + 20.0 + a = Account(MAXI_SAVINGS) + a.deposit(amount) + iy = a.interestEarned() + self.assertEqual(iy, interest) + + def test_interest_maxi_savings_max(self): + amount = 2350.00 + interest = (amount - 2000) * 0.1 + 70.0 + a = Account(MAXI_SAVINGS) + a.deposit(amount) + iy = a.interestEarned() + self.assertEqual(iy, interest) + From 7b116bf89b6f696abad06ae4c2ecd79d4597bcaf Mon Sep 17 00:00:00 2001 From: adroffner Date: Thu, 10 Dec 2015 23:12:06 -0800 Subject: [PATCH 06/11] add help docs to bank.py and improve code coverage. --- abcbank/bank.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/abcbank/bank.py b/abcbank/bank.py index 44711fe..fa720b9 100644 --- a/abcbank/bank.py +++ b/abcbank/bank.py @@ -1,25 +1,36 @@ -class Bank: +class Bank(object): + ''' A Bank branch has one or more Customers. + ''' + def __init__(self): + ''' Create a bank branch. + ''' self.customers = [] def addCustomer(self, customer): + ''' Add a new customer to the branch. + + :param Customer customer: add a new Customer object + ''' 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") + ")" - return summary - def _format(self, number, word): - return str(number) + " " + (word if (number == 1) else word + "s") + ''' List a summary of customers who have accounts at this branch. + + :returns: a customer summary string + ''' + summary = "Customer Summary\n - " + return summary + "\n - ".join([ str(customer) for customer in self.customers ]) + def totalInterestPaid(self): + ''' Calculate the total interest paid by the bank during this period. + + :returns: total interest in USD + :rtype: float + ''' 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 + + # def getFirstCustomer(self): There is no use for this method, and it was buggy! From addadf32d164a164e83a5f8ea91f91f3ca1d9976 Mon Sep 17 00:00:00 2001 From: adroffner Date: Fri, 11 Dec 2015 00:00:50 -0800 Subject: [PATCH 07/11] Refactor modules to put related facts where they belong. Remove dead code. Also, improve test coverage. --- abcbank/account.py | 16 +++++++++ abcbank/customer.py | 73 ++++++++++++++++++++++++++++-------------- abcbank/transaction.py | 29 +++++++++++++++-- 3 files changed, 92 insertions(+), 26 deletions(-) diff --git a/abcbank/account.py b/abcbank/account.py index 225609d..44ce461 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -70,3 +70,19 @@ def sumTransactions(self, checkAllTransactions=True): ''' return sum([t.amount for t in self.transactions]) + def accountTypeText(self): + ''' Give an account type description text. + + :returns: a string for the account type name + ''' + accountType = "" + + if self.accountType == CHECKING: + accountType = "Checking Account" + if self.accountType == SAVINGS: + accountType = "Savings Account" + if self.accountType == MAXI_SAVINGS: + accountType = "Maxi Savings Account" + + return accountType + diff --git a/abcbank/customer.py b/abcbank/customer.py index d953052..63078f5 100644 --- a/abcbank/customer.py +++ b/abcbank/customer.py @@ -1,55 +1,80 @@ from abcbank.account import CHECKING, SAVINGS, MAXI_SAVINGS -class Customer: +class Customer(object): + ''' A Customer of the bank. + ''' + def __init__(self, name): + ''' Create a new customer who has one or more accounts. + ''' self.name = name self.accounts = [] + def __str__(self): + return "{} ({} account{})".format(self.name, self.numAccs(), + ('s' if self.numAccs() != 1 else '')) + def openAccount(self, account): + ''' Open a new account for this customer. + The method returns this object rather than NoneType, like sorted() vs. sort(). + + :param account: a new Account object + :returns: this Customer object + ''' self.accounts.append(account) return self def numAccs(self): + ''' Show the number of accounts this customer has. + + :returns: number of accounts + :rtype: int + ''' return len(self.accounts) def totalInterestEarned(self): + ''' Compute the total interest earned by this customer across all accounts. + + :returns: total interest in USD + :rtype: float + ''' return sum([a.interestEarned() for a in self.accounts]) - # This method gets a statement def getStatement(self): + ''' Get a bank statement for this customer. + + :returns: a bank statement string + ''' + # DELETE: These lines do nothing and 1988 is before Python ever existed! # JIRA-123 Change by Joe Bloggs 29/7/1988 start - statement = None # reset statement to null here + ## 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) + accountStatements = [ self.statementForAccount(account) for account in self.accounts ] + + statement = ("Statement for {}".format(self.name) + ''.join(accountStatements) + + "\n\nTotal In All Accounts " + _toDollars(totalAcrossAllAccounts)) return statement 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)) + ''' Prepare a statement for a single account belonging to this customer. + + :param Account account: pass the Account object + :returns: a statement string for the account + ''' + transactionSummary = [t.eventText() + " " + _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" - else: - return "N/A" + return "\n\n{}\n".format(account.accountTypeText()) + transactionSummary + totalSummary def _toDollars(number): + ''' Format an amount in USD to a printed USD string, e.g. 12.01 to "$12.01". + + :param float number: amount in USD + :returns: a USD string to print + ''' return "${:1.2f}".format(number) diff --git a/abcbank/transaction.py b/abcbank/transaction.py index 8e5b5ad..7a0d68c 100644 --- a/abcbank/transaction.py +++ b/abcbank/transaction.py @@ -1,7 +1,32 @@ from datetime import datetime -class Transaction: +class Transaction(object): + ''' A single Transaction on an account. + + This expects the real numeric amount in USD. + A deposit is positive. + A withdrawl is negative. + ''' + def __init__(self, amount): + ''' Create a new transaction for the amount. + + :param float amount: amount deposited or withdrawn in USD + ''' self.amount = amount - self.transactionDate = datetime.now() \ No newline at end of file + self.transactionDate = datetime.now() + + def eventText(self): + ''' Transaction event text describes what happened. + + :returns: event description string + ''' + if self.amount < 0: + return "withdrawal" + elif self.amount > 0: + return "deposit" + else: + return "N/A" + + From 63d7faa8735a917ac533811b21d2f53b4f9b954a Mon Sep 17 00:00:00 2001 From: adroffner Date: Fri, 11 Dec 2015 01:16:25 -0800 Subject: [PATCH 08/11] Add the Customer.transfer() method that takes the two Accounts. --- abcbank/customer.py | 24 ++++++++++++++++++++++++ tests/customer_tests.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/abcbank/customer.py b/abcbank/customer.py index 63078f5..3148eeb 100644 --- a/abcbank/customer.py +++ b/abcbank/customer.py @@ -70,6 +70,30 @@ def statementForAccount(self, account): totalSummary = "Total " + _toDollars(sum([t.amount for t in account.transactions])) return "\n\n{}\n".format(account.accountTypeText()) + transactionSummary + totalSummary + def ownsAccount(self, account): + '''Does this customer own the `account`? + + :parameter Account account: the account to check + :returns: True when this Customer owns the Account + ''' + # Use the "is" operator to match the exact Account object. + # TODO: Replace this with a clear Account.id + return any([ account is v for v in self.accounts ]) + + def transfer(self, fromAccount, toAccount, amount): + ''' Transfer money from one account to another belonging to this customer. + The `amount` should be a postive value in USD. + + :param fromAccount: transfer from this Account + :param toAccount: transfer to this Account + :param float amount: transfer this amount in USD + ''' + if not (self.ownsAccount(fromAccount) and self.ownsAccount(toAccount)): + raise ValueError("Customer cannot transfer between those accounts") + + fromAccount.withdraw(amount) + toAccount.deposit(amount) + def _toDollars(number): ''' Format an amount in USD to a printed USD string, e.g. 12.01 to "$12.01". diff --git a/tests/customer_tests.py b/tests/customer_tests.py index da8032e..5840250 100644 --- a/tests/customer_tests.py +++ b/tests/customer_tests.py @@ -3,7 +3,7 @@ from abcbank.account import Account, CHECKING, SAVINGS from abcbank.customer import Customer -class StatementTests(TestCase): +class AccountTests(TestCase): def test_statement(self): checkingAccount = Account(CHECKING) @@ -35,3 +35,30 @@ def test_threeAccounts(self): oscar = Customer("Oscar").openAccount(Account(SAVINGS)) oscar.openAccount(Account(CHECKING)) self.assertEquals(oscar.numAccs(), 3) + + def test_ownsAccount_True(self): + oscar = Customer("Oscar").openAccount(Account(SAVINGS)) + checkingAcct = Account(CHECKING) + oscar.openAccount(checkingAcct) + self.assertTrue(oscar.ownsAccount(checkingAcct)) + + def test_ownsAccount_False(self): + oscar = Customer("Oscar").openAccount(Account(SAVINGS)) + oscar.openAccount(Account(CHECKING)) + checkingAcct = Account(CHECKING) + self.assertFalse(oscar.ownsAccount(checkingAcct)) + + def test_transfer(self): + savingsAcct = Account(SAVINGS) + checkingAcct = Account(CHECKING) + oscar = Customer("Oscar").openAccount(savingsAcct) + oscar.openAccount(checkingAcct) + # Start with $500 in savings and $0 in checking. + savingsAcct.deposit(500.00) + self.assertTrue(oscar.ownsAccount(savingsAcct)) + self.assertTrue(oscar.ownsAccount(checkingAcct)) + # Transfer half of savings to checking to have $250 each. + oscar.transfer(savingsAcct, checkingAcct, 250.00) + self.assertEquals(savingsAcct.sumTransactions(), 250.0) + self.assertEquals(checkingAcct.sumTransactions(), 250.0) + From 46aad20760af64c6112462a2a70523d168d466b3 Mon Sep 17 00:00:00 2001 From: adroffner Date: Fri, 11 Dec 2015 01:37:40 -0800 Subject: [PATCH 09/11] Refactor account type names to dict & get 100% coverage. --- abcbank/account.py | 15 +++++++-------- tests/account_tests.py | 7 ++++++- tests/customer_tests.py | 12 ++++++++++++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/abcbank/account.py b/abcbank/account.py index 44ce461..ff99ebc 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -4,6 +4,11 @@ SAVINGS = 1 MAXI_SAVINGS = 2 +ACCT_TYPE_NAME = { + CHECKING: "Checking Account", + SAVINGS: "Savings Account", + MAXI_SAVINGS: "Maxi Savings Account", +} class Account(object): ''' An Account belongs to a Customer. @@ -76,13 +81,7 @@ def accountTypeText(self): :returns: a string for the account type name ''' accountType = "" - - if self.accountType == CHECKING: - accountType = "Checking Account" - if self.accountType == SAVINGS: - accountType = "Savings Account" - if self.accountType == MAXI_SAVINGS: - accountType = "Maxi Savings Account" - + if self.accountType in ACCT_TYPE_NAME: + accountType = ACCT_TYPE_NAME[self.accountType] return accountType diff --git a/tests/account_tests.py b/tests/account_tests.py index d417b35..a678360 100644 --- a/tests/account_tests.py +++ b/tests/account_tests.py @@ -1,7 +1,7 @@ from unittest import TestCase from abcbank.transaction import Transaction -from abcbank.account import Account, CHECKING, SAVINGS, MAXI_SAVINGS +from abcbank.account import Account, CHECKING, SAVINGS, MAXI_SAVINGS, ACCT_TYPE_NAME class AccountTests(TestCase): @@ -80,3 +80,8 @@ def test_interest_maxi_savings_max(self): iy = a.interestEarned() self.assertEqual(iy, interest) + def test_accountTypeText(self): + for k,v in ACCT_TYPE_NAME.items(): + a = Account(k) + self.assertEquals(a.accountTypeText(), v) + diff --git a/tests/customer_tests.py b/tests/customer_tests.py index 5840250..1487acc 100644 --- a/tests/customer_tests.py +++ b/tests/customer_tests.py @@ -62,3 +62,15 @@ def test_transfer(self): self.assertEquals(savingsAcct.sumTransactions(), 250.0) self.assertEquals(checkingAcct.sumTransactions(), 250.0) + def test_transfer_error(self): + savingsAcct = Account(SAVINGS) + checkingAcct = Account(CHECKING) + otherAcct = Account(CHECKING) + oscar = Customer("Oscar").openAccount(savingsAcct) + oscar.openAccount(checkingAcct) + # Start with $500 in savings and $0 in checking. + savingsAcct.deposit(500.00) + # Try to transfer half of savings to other person's account to have $250 each. + with self.assertRaisesRegexp(ValueError, r'Customer cannot transfer between those accounts'): + oscar.transfer(savingsAcct, otherAcct, 250.00) + From b1d31e49f1bcf4ad1239431e4ea590cee89e51e5 Mon Sep 17 00:00:00 2001 From: adroffner Date: Fri, 11 Dec 2015 02:50:35 -0800 Subject: [PATCH 10/11] Add an Account.transactionHistory() test to find withdrawls in N days,etc. --- abcbank/account.py | 16 ++++++++++++++++ abcbank/transaction.py | 18 +++++++++++++----- tests/account_tests.py | 28 +++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/abcbank/account.py b/abcbank/account.py index ff99ebc..f54d963 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from abcbank.transaction import Transaction CHECKING = 0 @@ -85,3 +86,18 @@ def accountTypeText(self): accountType = ACCT_TYPE_NAME[self.accountType] return accountType + def transactionHistory(self, daysOld, transType=None): + ''' Account Transaction history. + + :param int daysOld: find transaction that are no more than N days old + :param transType: a Transaction type code, e.g. transaction.DEPOSIT + ''' + transList = [] + # Filter out older older transactions. + now = datetime.now() + transList = [ v for v in self.transactions if (now - v.transactionDate) <= timedelta(days=daysOld) ] + # Filter out transaction of the right type. + if transType: + transList = [ v for v in self.transactions if v.eventType() == transType ] + return transList + diff --git a/abcbank/transaction.py b/abcbank/transaction.py index 7a0d68c..36042cf 100644 --- a/abcbank/transaction.py +++ b/abcbank/transaction.py @@ -1,5 +1,7 @@ from datetime import datetime +DEPOSIT = "deposit" +WITHDRAWL = "withdrawal" class Transaction(object): ''' A single Transaction on an account. @@ -17,16 +19,22 @@ def __init__(self, amount): self.amount = amount self.transactionDate = datetime.now() - def eventText(self): - ''' Transaction event text describes what happened. + def eventType(self): + ''' Transaction event type code - :returns: event description string + :returns: event type string ''' if self.amount < 0: - return "withdrawal" + return WITHDRAWL elif self.amount > 0: - return "deposit" + return DEPOSIT else: return "N/A" + def eventText(self): + ''' Transaction event text describes what happened. + + :returns: event description string + ''' + return self.eventType() diff --git a/tests/account_tests.py b/tests/account_tests.py index a678360..54ee68c 100644 --- a/tests/account_tests.py +++ b/tests/account_tests.py @@ -1,6 +1,7 @@ from unittest import TestCase -from abcbank.transaction import Transaction +from datetime import datetime, timedelta +from abcbank.transaction import Transaction, DEPOSIT, WITHDRAWL from abcbank.account import Account, CHECKING, SAVINGS, MAXI_SAVINGS, ACCT_TYPE_NAME @@ -85,3 +86,28 @@ def test_accountTypeText(self): a = Account(k) self.assertEquals(a.accountTypeText(), v) + def test_transactionHistory_age_ok(self): + amount = 250.00 + a = Account(MAXI_SAVINGS) + a.deposit(amount) + a.transactions[0].transactionDate = datetime.now() - timedelta(days=0) + result = a.transactionHistory(5) + self.assertEquals(len(result), 1) + + def test_transactionHistory_age_older(self): + amount = 250.00 + a = Account(MAXI_SAVINGS) + a.deposit(amount) + a.transactions[0].transactionDate = datetime.now() - timedelta(days=8) + result = a.transactionHistory(5) + self.assertEquals(len(result), 0) + + def test_transactionHistory_type_ok(self): + amount = 250.00 + a = Account(MAXI_SAVINGS) + a.deposit(amount) + a.withdraw(amount) + a.deposit(amount) + result = a.transactionHistory(5, transType=DEPOSIT) + self.assertEquals(len(result), 2) + From da385bec538885275ad9788ffed837fa2a5e9ea4 Mon Sep 17 00:00:00 2001 From: adroffner Date: Fri, 11 Dec 2015 03:16:19 -0800 Subject: [PATCH 11/11] Change the MAXI_SAVINGS plan fetures to the new rules. --- abcbank/account.py | 10 +++++++++- tests/account_tests.py | 20 +++++++------------- tests/bank_tests.py | 4 ++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/abcbank/account.py b/abcbank/account.py index f54d963..5a695b1 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from abcbank.transaction import Transaction +from abcbank.transaction import Transaction, DEPOSIT, WITHDRAWL CHECKING = 0 SAVINGS = 1 @@ -58,12 +58,20 @@ def interestEarned(self): else: return 1 + (amount - 1000) * 0.002 elif self.accountType == MAXI_SAVINGS: + # NOTE: This was the former maxi-savings plan + """ if (amount <= 1000): return amount * 0.02 elif (amount <= 2000): return 20 + (amount - 1000) * 0.05 else: return 70 + (amount - 2000) * 0.1 + """ + # Give a lower rate when withdrawls were made in the last 10 days. + if self.transactionHistory(10, transType=WITHDRAWL): + return amount * 0.001 + else: + return amount * 0.05 elif self.accountType == CHECKING: return amount * 0.001 else: diff --git a/tests/account_tests.py b/tests/account_tests.py index 54ee68c..e223815 100644 --- a/tests/account_tests.py +++ b/tests/account_tests.py @@ -57,27 +57,21 @@ def test_interest_savings_high(self): iy = a.interestEarned() self.assertEqual(iy, interest) - def test_interest_maxi_savings_low(self): + def test_interest_maxi_savings_no_withdrawl(self): amount = 370.00 - interest = amount * 0.02 + interest = amount * 0.05 a = Account(MAXI_SAVINGS) a.deposit(amount) iy = a.interestEarned() self.assertEqual(iy, interest) - def test_interest_maxi_savings_high(self): - amount = 1250.00 - interest = (amount - 1000) * 0.05 + 20.0 - a = Account(MAXI_SAVINGS) - a.deposit(amount) - iy = a.interestEarned() - self.assertEqual(iy, interest) - - def test_interest_maxi_savings_max(self): - amount = 2350.00 - interest = (amount - 2000) * 0.1 + 70.0 + def test_interest_maxi_savings_recent_withdrawl(self): + amount = 370.00 + pocketMoney = 50.00 + interest = (amount - pocketMoney) * 0.001 a = Account(MAXI_SAVINGS) a.deposit(amount) + a.withdraw(pocketMoney) iy = a.interestEarned() self.assertEqual(iy, interest) diff --git a/tests/bank_tests.py b/tests/bank_tests.py index f73e316..fbea480 100644 --- a/tests/bank_tests.py +++ b/tests/bank_tests.py @@ -4,7 +4,7 @@ from abcbank.bank import Bank from abcbank.customer import Customer -class CustomerTests(TestCase): +class BankTests(TestCase): def test_customer_summary(self): bank = Bank() @@ -36,4 +36,4 @@ def test_maxi_savings_account(self): checkingAccount = Account(MAXI_SAVINGS) bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) checkingAccount.deposit(3000.0) - self.assertEquals(bank.totalInterestPaid(), 170.0) + self.assertEquals(bank.totalInterestPaid(), 150.0)