diff --git a/abcbank/date_provider.py b/abcbank/date_provider.py index 33b64eb..36f1737 100644 --- a/abcbank/date_provider.py +++ b/abcbank/date_provider.py @@ -1,7 +1,22 @@ +""" +Class: DateProvider + +Note: + +now method is modified to return UTC current date and time. +customedDate method is added to allow user to provide a backdated date if necessary. + + +""" from datetime import datetime class DateProvider: @staticmethod def now(): - return datetime.now() \ No newline at end of file + # Make sure all the dates are in UTC timezone + return datetime.utcnow() + + @staticmethod + def customedDate(year, month, day): + return datetime(year, month, day) diff --git a/account.py b/account.py new file mode 100644 index 0000000..7e50215 --- /dev/null +++ b/account.py @@ -0,0 +1,93 @@ +""" +Class: Account + +Note: + +__init__ is changed to accept one more parameter, openDate. +openDate is the date the account is opened. It is used for interest calculation. +If it is not passed during instantiation, openDate will be defaulted to current UTC datetime. + +deposit and withdraw methods are also modified to accept one more parameter, transDate. +transDate is used for calculating interest rate. It is particuarly useful for maxi_saving_accounts interest calculation. +If transDate is not set, it will be defaulted to current UTC datetime. + +There are two interest calculations: simple daily accrued interest (_cal_Simple_AccruedInt) +and compound daily interest (_cal_Compound_AccruedInt). +Since I am not sure which solution you prefer, it is currently set to use simple daily accrued interest calcuation. + +sumTransactions method is modified to calculate the amount of withdrawls for the past 10 days when checkAllTransactions +is set to False. It is particuarly useful for maxi_saving_accounts interest calculation. + +""" + +from abcbank.transaction import Transaction +from abcbank.date_provider import DateProvider + +CHECKING = 0 +SAVINGS = 1 +MAXI_SAVINGS = 2 + +class Account: + def __init__(self, accountType, openDate=None): + self.accountType = accountType + self.transactions = [] + if openDate is None: + self.openDate = DateProvider().now() + else: + self.openDate = openDate + + def deposit(self, amount, transDate=None): + if (amount <= 0): + raise ValueError("amount must be greater than zero") + else: + self.transactions.append(Transaction(amount,transDate)) + + + def withdraw(self, amount, transDate=None): + if (amount <= 0): + raise ValueError("amount must be greater than zero") + else: + # Find out if the account has enough money for withdrawl. + if self.sumTransactions() < amount: + raise Exception("Withdrawal amount exceeds the balance of the account.") + else: + self.transactions.append(Transaction(-amount,transDate)) + + + def _cal_Simple_AccruedInt(self, amount, rate): + days_accumlated = (DateProvider().now() - self.openDate).days + return amount * (rate / 365) * days_accumlated + + def _cal_Compound_AccruedInt(self, amount, rate): + days_accumlated = (DateProvider().now() - self.openDate).days + #return amount * (1 + rate/365) ** 365 + return amount * (1 + rate/365) ** days_accumlated + + def interestEarned(self): + amount = self.sumTransactions() + + interestFunc = self._cal_Simple_AccruedInt + #interestFunc = self._cal_Compound_AccruedInt + + if self.accountType == SAVINGS: + if (amount <= 1000): + return interestFunc(amount,0.001) + else: + return interestFunc(1000,0.001) + interestFunc((amount-1000),0.002) + + if self.accountType == MAXI_SAVINGS: + ten_days_withdrawl_amt = self.sumTransactions(checkAllTransactions=False) + if ten_days_withdrawl_amt > 0: + return interestFunc(amount, 0.001) + else: + return interestFunc(amount, 0.05) + else: + return interestFunc(amount,0.001) + + def sumTransactions(self, checkAllTransactions=True): + if checkAllTransactions: + return sum([t.amount for t in self.transactions]) + else: + # Only calculate the sum of withdrawls for the past 10 days + now = DateProvider().now() + return -1 * sum([t.amount for t in self.transactions if (0 <= (now - t.transactionDate).days <=10) and t.amount < 0]) diff --git a/bank.py b/bank.py new file mode 100644 index 0000000..683da9d --- /dev/null +++ b/bank.py @@ -0,0 +1,34 @@ +""" +Class: Bank + +Note: + +getFirstCustomer method is fixed. It only prints out the firstCustomer name if there is a customer. + +""" + +from abcbank.customer import Customer + +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") + ")" + 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): + if len(self.customers) == 0: + raise Exception("Bank has no customers.") + return self.customers[0].name + diff --git a/customer.py b/customer.py new file mode 100644 index 0000000..f96d9bf --- /dev/null +++ b/customer.py @@ -0,0 +1,76 @@ +""" +Class: Customer + +Note: + +Added transferAccounts method to allow user to transfer money between his/her accounts in the bank. + +In statementForAccount method, totalSummary is changed to use account.sumTransactions() method. + +""" + +from abcbank.account import CHECKING, SAVINGS, MAXI_SAVINGS + +class Customer: + def __init__(self, name): + self.name = name + self.accounts = [] + + def openAccount(self, account): + + self.accounts.append(account) + return self + + def transferAccounts(self, fromAcct, toAcct, amount): + + # Make sure there is sufficent amount of money in the fromAcct before transfer + fromAcctBal = fromAcct.sumTransactions() + if fromAcctBal == 0 or fromAcctBal < amount: + raise Exception('The source account does not have sufficient funds for transfer.') + + fromAcct.withdraw(amount) + toAcct.deposit(amount) + + def numAccs(self): + return len(self.accounts) + + def totalInterestEarned(self): + return sum([a.interestEarned() for a in self.accounts]) + + # 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 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(account.sumTransactions()) + return accountType + transactionSummary + totalSummary + + def withdrawalOrDepositText(self, transaction): + if transaction.amount < 0: + return "withdrawal" + elif transaction.amount > 0: + return "deposit" + else: + return "N/A" + + +def _toDollars(number): + return "${:1.2f}".format(number) diff --git a/tests/bank_tests.py b/tests/bank_tests.py index 6de98db..897f5a5 100644 --- a/tests/bank_tests.py +++ b/tests/bank_tests.py @@ -1,9 +1,25 @@ -from nose.tools import assert_equals +""" +Note: -from account import Account, CHECKING, MAXI_SAVINGS, SAVINGS -from bank import Bank -from customer import Customer +There are two sets of interest calculation tests: Simple Daily Interest and Compound Daily Interest. +The Compound Daily Interest set is turned off. Please turn it on if that is the calculation you are looking for. +Make sure interestFunc is set to self._cal_Compound_AccruedInt in interestEarned method in Account class file. +""" + +from nose.tools import assert_equals, assert_raises, nottest + +from abcbank.account import Account, CHECKING, MAXI_SAVINGS, SAVINGS +from abcbank.bank import Bank +from abcbank.customer import Customer + +from datetime import datetime, time +from dateutil.relativedelta import relativedelta + +# Figure out the date one year from now. +# This is easier for calculate the interest +backDated_one_year = datetime.utcnow().date() + relativedelta(days=-365) +backDated_one_year = datetime.combine(backDated_one_year, time.min) def test_customer_summary(): bank = Bank() @@ -12,27 +28,83 @@ def test_customer_summary(): assert_equals(bank.customerSummary(), "Customer Summary\n - John (1 account)") +def test_firstCustomer(): + bank = Bank() + assert_raises(Exception, bank.getFirstCustomer) + def test_checking_account(): + # Simple Daily Interest Calculation Test. bank = Bank() - checkingAccount = Account(CHECKING) + checkingAccount = Account(CHECKING,backDated_one_year) bill = Customer("Bill").openAccount(checkingAccount) bank.addCustomer(bill) checkingAccount.deposit(100.0) - assert_equals(bank.totalInterestPaid(), 0.1) + assert_equals(bank.totalInterestPaid(), 0.10000000000000002) def test_savings_account(): + # Simple Daily Interest Calculation Test. bank = Bank() - checkingAccount = Account(SAVINGS) + checkingAccount = Account(SAVINGS,backDated_one_year) bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) checkingAccount.deposit(1500.0) - assert_equals(bank.totalInterestPaid(), 2.0) + assert_equals(bank.totalInterestPaid(), 2.00) def test_maxi_savings_account(): + # Simple Daily Interest Calculation Test. bank = Bank() - checkingAccount = Account(MAXI_SAVINGS) + checkingAccount = Account(MAXI_SAVINGS,backDated_one_year) 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(), 150.0) + + +def test_maxi_savings_acct_diffInt(): + # Simple Daily Interest Calculation Test. + bank = Bank() + checkingAccount = Account(MAXI_SAVINGS,backDated_one_year) + bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) + checkingAccount.deposit(3000.0) + checkingAccount.withdraw(1000.0) + assert_equals(bank.totalInterestPaid(), 2.0) + +@nottest +def test_checking_account_2(): + # Compond Daily Interest Calculation Test. + bank = Bank() + checkingAccount = Account(CHECKING,backDated_one_year) + bill = Customer("Bill").openAccount(checkingAccount) + bank.addCustomer(bill) + checkingAccount.deposit(100.0) + assert_equals(bank.totalInterestPaid(), 100.10004987954706 ) + +@nottest +def test_savings_account_2(): + # Compond Daily Interest Calculation Test. + bank = Bank() + checkingAccount = Account(SAVINGS,backDated_one_year) + bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) + checkingAccount.deposit(1500.0) + assert_equals(bank.totalInterestPaid(), 1502.0014967172629) + +@nottest +def test_maxi_savings_account_2(): + # Compond Daily Interest Calculation Test. + bank = Bank() + checkingAccount = Account(MAXI_SAVINGS,backDated_one_year) + bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) + checkingAccount.deposit(3000.0) + assert_equals(bank.totalInterestPaid(), 3153.802489402342) + +@nottest +def test_maxi_savings_acct_diffInt_2(): + # Compond Daily Interest Calculation Test. + bank = Bank() + checkingAccount = Account(MAXI_SAVINGS,backDated_one_year) + bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) + checkingAccount.deposit(3000.0) + checkingAccount.withdraw(1000.0) + assert_equals(bank.totalInterestPaid(), 2002.000997590941) + diff --git a/tests/customer_tests.py b/tests/customer_tests.py index 0211a4f..6b650e3 100644 --- a/tests/customer_tests.py +++ b/tests/customer_tests.py @@ -1,7 +1,7 @@ -from nose.tools import assert_equals, nottest +from nose.tools import assert_equals, assert_raises, nottest -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(): @@ -29,8 +29,33 @@ 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_withdrawl_error(): + checkingAccount = Account(CHECKING) + david = Customer("David").openAccount(checkingAccount) + checkingAccount.deposit(100.0) + assert_raises(Exception, checkingAccount.withdraw, 200) + +def test_transfer(): + checkingAccount = Account(CHECKING) + savingsAccount = Account(SAVINGS) + henry = Customer("Henry").openAccount(checkingAccount).openAccount(savingsAccount) + checkingAccount.deposit(100.0) + savingsAccount.deposit(4000.0) + henry.transferAccounts(savingsAccount, checkingAccount, 1000) + assert_equals(savingsAccount.sumTransactions(), 3000) + assert_equals(checkingAccount.sumTransactions(), 1100) + +def test_transfer_with_insufficientfund(): + checkingAccount = Account(CHECKING) + savingsAccount = Account(SAVINGS) + john = Customer("John").openAccount(checkingAccount).openAccount(savingsAccount) + checkingAccount.deposit(100.0) + savingsAccount.deposit(4000.0) + assert_raises(Exception, john.transferAccounts, (savingsAccount, checkingAccount, 5000)) + diff --git a/tests/transaction_tests.py b/tests/transaction_tests.py index 62caa8a..a16d66c 100644 --- a/tests/transaction_tests.py +++ b/tests/transaction_tests.py @@ -1,8 +1,13 @@ -from nose.tools import assert_is_instance - -from transaction import Transaction +from nose.tools import assert_is_instance, assert_equal +from abcbank.transaction import Transaction +from abcbank.date_provider import DateProvider +from datetime import datetime 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") + +def test_transDate(): + t = Transaction(10, DateProvider().customedDate(2016, 8, 20)) + assert_equal(t.transactionDate, datetime(2016, 8, 20)) diff --git a/transaction.py b/transaction.py new file mode 100644 index 0000000..1a55e0d --- /dev/null +++ b/transaction.py @@ -0,0 +1,21 @@ +""" +Class: Transaction + +Note: + +The class init method is modified to take an additional parameter: transDate. +transDate is for interest calculation purpose. If transDate is not passed, +it will be defaulted to current UTC datetime. + +""" + + +from abcbank.date_provider import DateProvider + +class Transaction: + def __init__(self, amount, transDate=None): + self.amount = amount + if transDate is None: + self.transactionDate = DateProvider.now() + else: + self.transactionDate = transDate