diff --git a/abcbank/account.py b/abcbank/account.py index e010009..8469e44 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -1,43 +1,142 @@ +from datetime import datetime + from abcbank.transaction import Transaction + CHECKING = 0 SAVINGS = 1 MAXI_SAVINGS = 2 +DAYS_IN_YEAR = 365 class Account: - def __init__(self, accountType): - self.accountType = accountType + def __init__(self): self.transactions = [] - def deposit(self, amount): + def deposit(self, amount, transactionDate=None): if (amount <= 0): raise ValueError("amount must be greater than zero") else: - self.transactions.append(Transaction(amount)) + self.transactions.append(Transaction(amount, transactionDate)) - def withdraw(self, amount): + def withdraw(self, amount, transactionDate=None): if (amount <= 0): raise ValueError("amount must be greater than zero") else: - self.transactions.append(Transaction(-amount)) + self.transactions.append(Transaction(-amount, transactionDate)) + + def _calculateInterest(self, checkAllTransactionsUpInToIndex=None, daysAccrued=365): + fractionOfYearAccrued = daysAccrued/DAYS_IN_YEAR + amount = self.sumTransactions(checkAllTransactionsUpInToIndex) + interestAmount = 0.01 + - def interestEarned(self): - amount = self.sumTransactions() if self.accountType == SAVINGS: if (amount <= 1000): - return amount * 0.001 + interestAmount = 0.001 + return amount * interestAmount * fractionOfYearAccrued else: - return 1 + (amount - 1000) * 0.002 - if self.accountType == MAXI_SAVINGS: + interestAmount = 0.002 + return 1 + (amount - 1000) * interestAmount * fractionOfYearAccrued + elif self.accountType == MAXI_SAVINGS: if (amount <= 1000): - return amount * 0.02 + currTime = datetime.now() + interestAmount = 0.02 + if abs(self.transactions[-1].transactionDate - currTime <= datetime.timedelta(days=10)): + interestAmount = 0.01 + return amount * interestAmount * fractionOfYearAccrued elif (amount <= 2000): - return 20 + (amount - 1000) * 0.05 + interestAmount = 0.05 + return 20 + (amount - 1000) * interestAmount * fractionOfYearAccrued else: - return 70 + (amount - 2000) * 0.1 + interestAmount = 0.1 + return 70 + (amount - 2000) * interestAmount * fractionOfYearAccrued else: - return amount * 0.001 + return amount * interestAmount * fractionOfYearAccrued + + def sumTransactions(self, checkAllTransactionsUpToIndex=None): + endIndex = checkAllTransactionsUpToIndex or len(self.transactions) + return sum([t.amount for t in self.transactions[0:endIndex]]) + + def interestEarned(self): + """Calculate Accrued Interest (daily)""" + + transactions = self.transactions + totalInterest = 0 + + for ind in range(1, len(transactions)): + prevTransaction = transactions[ind-1] + currTransaction = transactions[ind] + timeLagDays = (currTransaction.transactionDate - prevTransaction.transactionDate).days + totalInterest += self._calculateInterest(checkAllTransactionsUpInToIndex=ind, daysAccrued=timeLagDays) + totalInterest += self._calculateInterest( + daysAccrued=(datetime.now() - self.transactions[-1].transactionDate).days) + return totalInterest + + @property + def accountDescription(self): + return 'Account' + +class CheckingAccount(Account): + accountType = CHECKING + + def _calculateInterest(self, checkAllTransactionsUpInToIndex=None, daysAccrued=365): + fractionOfYearAccrued = daysAccrued / DAYS_IN_YEAR + amount = self.sumTransactions(checkAllTransactionsUpInToIndex) + return amount * 0.001 * fractionOfYearAccrued + + @property + def accountDescription(self): + return "Checking Account" +class SavingsAccount(Account): + accountType = SAVINGS + + def _calculateInterest(self, checkAllTransactionsUpInToIndex=None, daysAccrued=365): + fractionOfYearAccrued = daysAccrued / DAYS_IN_YEAR + amount = self.sumTransactions(checkAllTransactionsUpInToIndex) + + if (amount <= 1000): + interestAmount = 0.001 + return amount * interestAmount * fractionOfYearAccrued + else: + interestAmount = 0.002 + return 1 + (amount - 1000) * interestAmount * fractionOfYearAccrued + + @property + def accountDescription(self): + return "Savings Account" + + +class MaxiSavings(Account): + accountType = MAXI_SAVINGS + + def _calculateInterest(self, checkAllTransactionsUpInToIndex=None, daysAccrued=365): + fractionOfYearAccrued = daysAccrued / DAYS_IN_YEAR + amount = self.sumTransactions(checkAllTransactionsUpInToIndex) + + if (amount <= 1000): + currTime = datetime.now() + interestAmount = 0.02 + if abs(self.transactions[-1].transactionDate - currTime <= datetime.timedelta(days=10)): + interestAmount = 0.01 + return amount * interestAmount * fractionOfYearAccrued + elif (amount <= 2000): + interestAmount = 0.05 + return 20 + (amount - 1000) * interestAmount * fractionOfYearAccrued + else: + interestAmount = 0.1 + return 70 + (amount - 2000) * interestAmount * fractionOfYearAccrued + + @property + def accountDescription(self): + return "Maxi Savings Account" + - def sumTransactions(self, checkAllTransactions=True): - return sum([t.amount for t in self.transactions]) \ No newline at end of file +def createBankAccount(accountType): + if accountType == CHECKING: + return CheckingAccount() + elif accountType == SAVINGS: + return SavingsAccount() + elif accountType == MAXI_SAVINGS: + return MaxiSavings() + raise ValueError("Account Type: {AccountType} is not supported".format(accountType)) diff --git a/abcbank/bank.py b/abcbank/bank.py index 44711fe..355b26c 100644 --- a/abcbank/bank.py +++ b/abcbank/bank.py @@ -4,22 +4,17 @@ def __init__(self): 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 + summary = ''.join(["\n - {} ({})".format(c.name, self._format(c.numAccs(), "account")) + for c in self.customers]) + return "Customer Summary{summary}" .format(summary=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 total \ No newline at end of file diff --git a/abcbank/customer.py b/abcbank/customer.py index 7cfd62a..5eccfa3 100644 --- a/abcbank/customer.py +++ b/abcbank/customer.py @@ -16,39 +16,26 @@ def numAccs(self): 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 + """This method returns a statement""" 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 + statements = ''.join([self.statementForAccount(account) for account in self.accounts]) + totalValue = _toDollars(totalAcrossAllAccounts) + + return "Statement for {name}{statements}\n\nTotal In All Accounts {totalValue}".format(name=self.name, + statements=statements, + totalValue=_toDollars(totalAcrossAllAccounts)) 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)) + accountType = "\n\n{accountDescription}\n".format(accountDescription=account.accountDescription) + transactionSummary = [t.transactionDescription() + " " + _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 "{accountType}{transactionSummary}{totalSummary}".format(accountType=accountType, + transactionSummary=transactionSummary, + totalSummary=totalSummary) def _toDollars(number): diff --git a/abcbank/date_provider.py b/abcbank/date_provider.py deleted file mode 100644 index 33b64eb..0000000 --- a/abcbank/date_provider.py +++ /dev/null @@ -1,7 +0,0 @@ -from datetime import datetime - - -class DateProvider: - @staticmethod - def now(): - return datetime.now() \ No newline at end of file diff --git a/abcbank/transaction.py b/abcbank/transaction.py index 8e5b5ad..c39cb9f 100644 --- a/abcbank/transaction.py +++ b/abcbank/transaction.py @@ -2,6 +2,11 @@ class Transaction: - def __init__(self, amount): + def __init__(self, amount, transactionDate=None): self.amount = amount - self.transactionDate = datetime.now() \ No newline at end of file + self.transactionDate = transactionDate or datetime.now() + + def transactionDescription(self): + if self.amount < 0: + return "withdrawal" + return "deposit" diff --git a/tests/bank_tests.py b/tests/bank_tests.py index 6de98db..1925700 100644 --- a/tests/bank_tests.py +++ b/tests/bank_tests.py @@ -1,13 +1,17 @@ -from nose.tools import assert_equals +import datetime -from account import Account, CHECKING, MAXI_SAVINGS, SAVINGS +from nose.tools import assert_equals, assert_almost_equals + +from account import Account, CHECKING, MAXI_SAVINGS, SAVINGS, createBankAccount from bank import Bank from customer import Customer +ONE_YEAR_AGO = datetime.datetime.today() - datetime.timedelta(days=365) + def test_customer_summary(): bank = Bank() - john = Customer("John").openAccount(Account(CHECKING)) + john = Customer("John").openAccount(createBankAccount(CHECKING)) bank.addCustomer(john) assert_equals(bank.customerSummary(), "Customer Summary\n - John (1 account)") @@ -15,24 +19,33 @@ def test_customer_summary(): def test_checking_account(): bank = Bank() - checkingAccount = Account(CHECKING) + checkingAccount = createBankAccount(CHECKING) bill = Customer("Bill").openAccount(checkingAccount) bank.addCustomer(bill) - checkingAccount.deposit(100.0) + checkingAccount.deposit(100.0, ONE_YEAR_AGO) assert_equals(bank.totalInterestPaid(), 0.1) -def test_savings_account(): +def test_savings_account(self): bank = Bank() - checkingAccount = Account(SAVINGS) + checkingAccount = createBankAccount(SAVINGS) bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) - checkingAccount.deposit(1500.0) + checkingAccount.deposit(1500.0, ONE_YEAR_AGO) assert_equals(bank.totalInterestPaid(), 2.0) -def test_maxi_savings_account(): +def test_daily_accrued_interest(self): bank = Bank() - checkingAccount = Account(MAXI_SAVINGS) + checkingAccount = createBankAccount(CHECKING) bank.addCustomer(Customer("Bill").openAccount(checkingAccount)) - checkingAccount.deposit(3000.0) - assert_equals(bank.totalInterestPaid(), 170.0) \ No newline at end of file + + two_years_ago = datetime.datetime.today() - datetime.timedelta(days=730) + checkingAccount.deposit(1000.0, transactionDate=two_years_ago) + assert_equals(checkingAccount.interestEarned(), 2.0) + + checkingAccount.deposit(1000.0, transactionDate=ONE_YEAR_AGO) + assert_almost_equals(checkingAccount.interestEarned(), 3.0, places=2) + + half_year_ago = datetime.datetime.today() - datetime.timedelta(days=182) + checkingAccount.deposit(1000.0, transactionDate=half_year_ago) + assert_almost_equals(checkingAccount.interestEarned(), 3.5, places=2) \ No newline at end of file diff --git a/tests/customer_tests.py b/tests/customer_tests.py index 0211a4f..fb19800 100644 --- a/tests/customer_tests.py +++ b/tests/customer_tests.py @@ -1,12 +1,12 @@ from nose.tools import assert_equals, nottest -from account import Account, CHECKING, SAVINGS +from account import Account, CHECKING, SAVINGS, createBankAccount from customer import Customer def test_statement(): - checkingAccount = Account(CHECKING) - savingsAccount = Account(SAVINGS) + checkingAccount = createBankAccount(CHECKING) + savingsAccount = createBankAccount(SAVINGS) henry = Customer("Henry").openAccount(checkingAccount).openAccount(savingsAccount) checkingAccount.deposit(100.0) savingsAccount.deposit(4000.0) @@ -19,18 +19,18 @@ def test_statement(): def test_oneAccount(): - oscar = Customer("Oscar").openAccount(Account(SAVINGS)) + oscar = Customer("Oscar").openAccount(createBankAccount(SAVINGS)) assert_equals(oscar.numAccs(), 1) def test_twoAccounts(): - oscar = Customer("Oscar").openAccount(Account(SAVINGS)) - oscar.openAccount(Account(CHECKING)) + oscar = Customer("Oscar").openAccount(createBankAccount(SAVINGS)) + oscar.openAccount(createBankAccount(CHECKING)) assert_equals(oscar.numAccs(), 2) @nottest def test_threeAccounts(): - oscar = Customer("Oscar").openAccount(Account(SAVINGS)) - oscar.openAccount(Account(CHECKING)) + oscar = Customer("Oscar").openAccount(createBankAccount(SAVINGS)) + oscar.openAccount(createBankAccount(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..6bf9eec 100644 --- a/tests/transaction_tests.py +++ b/tests/transaction_tests.py @@ -1,8 +1,16 @@ -from nose.tools import assert_is_instance +from nose.tools import assert_is_instance, assert_equals from 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") + + +def test_description(): + t = Transaction(-4) + assert_equals(t.transactionDescription(), "withdrawal") + + t = Transaction(5) + assert_equals(t.transactionDescription(), "deposit") \ No newline at end of file