diff --git a/abcbank/account.py b/abcbank/account.py index e010009..5a695b1 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -1,43 +1,111 @@ -from abcbank.transaction import Transaction +from datetime import datetime, timedelta +from abcbank.transaction import Transaction, DEPOSIT, WITHDRAWL CHECKING = 0 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. + ''' -class Account: 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("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): + ''' 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("amount must be greater than zero") + 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: + # 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 - else: + """ + # 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: + raise ValueError("account has an invalid account type") def sumTransactions(self, checkAllTransactions=True): - return sum([t.amount for t in self.transactions]) \ No newline at end of file + ''' Computes all deposits and withdrawls during the period. + + :returns: current monies without interest in USD + ''' + 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 in ACCT_TYPE_NAME: + 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/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! diff --git a/abcbank/customer.py b/abcbank/customer.py index 7cfd62a..3148eeb 100644 --- a/abcbank/customer.py +++ b/abcbank/customer.py @@ -1,55 +1,104 @@ -from account import CHECKING, SAVINGS, MAXI_SAVINGS +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 + 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 withdrawalOrDepositText(self, transaction): - if transaction.amount < 0: - return "withdrawal" - elif transaction.amount > 0: - return "deposit" - else: - return "N/A" + 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". + + :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..36042cf 100644 --- a/abcbank/transaction.py +++ b/abcbank/transaction.py @@ -1,7 +1,40 @@ from datetime import datetime +DEPOSIT = "deposit" +WITHDRAWL = "withdrawal" + +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. + ''' -class Transaction: 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 eventType(self): + ''' Transaction event type code + + :returns: event type string + ''' + if self.amount < 0: + return WITHDRAWL + elif self.amount > 0: + 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 new file mode 100644 index 0000000..e223815 --- /dev/null +++ b/tests/account_tests.py @@ -0,0 +1,107 @@ +from unittest import TestCase + +from datetime import datetime, timedelta +from abcbank.transaction import Transaction, DEPOSIT, WITHDRAWL +from abcbank.account import Account, CHECKING, SAVINGS, MAXI_SAVINGS, ACCT_TYPE_NAME + + +class AccountTests(TestCase): + + def test_deposit(self): + 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.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.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.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_no_withdrawl(self): + amount = 370.00 + interest = amount * 0.05 + a = Account(MAXI_SAVINGS) + a.deposit(amount) + iy = a.interestEarned() + self.assertEqual(iy, interest) + + 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) + + def test_accountTypeText(self): + for k,v in ACCT_TYPE_NAME.items(): + 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) + diff --git a/tests/bank_tests.py b/tests/bank_tests.py index 6de98db..fbea480 100644 --- a/tests/bank_tests.py +++ b/tests/bank_tests.py @@ -1,38 +1,39 @@ -from nose.tools import assert_equals - -from account import Account, CHECKING, MAXI_SAVINGS, SAVINGS -from bank import Bank -from 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) \ No newline at end of file +from unittest import TestCase + +from abcbank.account import Account, CHECKING, MAXI_SAVINGS, SAVINGS +from abcbank.bank import Bank +from abcbank.customer import Customer + +class BankTests(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(), 150.0) diff --git a/tests/customer_tests.py b/tests/customer_tests.py index 0211a4f..1487acc 100644 --- a/tests/customer_tests.py +++ b/tests/customer_tests.py @@ -1,36 +1,76 @@ -from nose.tools import assert_equals, nottest +from unittest import TestCase, skip -from account import Account, CHECKING, SAVINGS -from customer import Customer +from abcbank.account import Account, CHECKING, SAVINGS +from abcbank.customer import Customer +class AccountTests(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) -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_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_oneAccount(): - oscar = Customer("Oscar").openAccount(Account(SAVINGS)) - assert_equals(oscar.numAccs(), 1) + 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) + 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) -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) \ No newline at end of file diff --git a/tests/transaction_tests.py b/tests/transaction_tests.py index 62caa8a..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 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 +class TransactionTests(TestCase): + + def test_type(self): + t = Transaction(5) + self.assertIsInstance(t, Transaction, "correct type")