diff --git a/abcbank/account.py b/abcbank/account.py index e010009..fc31c37 100644 --- a/abcbank/account.py +++ b/abcbank/account.py @@ -1,3 +1,4 @@ +from abcbank.date_provider import DateUtils from abcbank.transaction import Transaction CHECKING = 0 @@ -9,35 +10,138 @@ class Account: def __init__(self, accountType): self.accountType = accountType self.transactions = [] + self.availableBalance = 0 - def deposit(self, amount): - if (amount <= 0): + def deposit(self, amount, date_deposited=None): + """ + + :param amount: amount to be deposited + :param date_deposited: optional param; date of deposit + :return: + """ + if amount <= 0: raise ValueError("amount must be greater than zero") else: - self.transactions.append(Transaction(amount)) + if date_deposited: + try: + date = DateUtils.toDate(date_deposited) + self.transactions.append(Transaction(amount, date)) + except ValueError: + return ValueError("Incorrect date format") + else: + self.transactions.append(Transaction(amount)) + self.availableBalance += amount def withdraw(self, amount): - if (amount <= 0): + """ + + :param amount: amount to be withdrawn + :return: None + """ + if amount <= 0: raise ValueError("amount must be greater than zero") + elif amount > self.availableBalance: + raise ValueError("amount greater than available balance") else: self.transactions.append(Transaction(-amount)) + self.availableBalance -= amount def interestEarned(self): - amount = self.sumTransactions() + """ + calculate the interest annually + + :return: interest payout for the customer + """ + amount = self.availableBalance if self.accountType == SAVINGS: - if (amount <= 1000): + if amount <= 1000: return 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 + endDate = DateUtils.now() + startDate = DateUtils.add(endDate, -10) + _transactions = self.getTransaction(startDate, endDate) + withdrawn = 0 + for t in _transactions: + if t.amount < 0: + withdrawn = 1 + break + if withdrawn == 1: + return amount * (0.1 / 100.0) else: - return 70 + (amount - 2000) * 0.1 + return amount * (5 / 100.0) else: return amount * 0.001 + def interest_earned_after_x_days(self, days=1): + """ + Calculate the interest after x days + + :param days: + :return: return the Amount along with accrued interest + """ + amount = self.availableBalance + if self.accountType == SAVINGS: + if amount <= 1000: + return Account.daily_interest(amount, 0.1, days) + else: + return Account.daily_interest(amount - 1000, 0.2, days) + Account.daily_interest(1000, 0.1, days) + if self.accountType == MAXI_SAVINGS: + endDate = DateUtils.now() + startDate = DateUtils.add(endDate, -10) + _transactions = self.getTransaction(startDate, endDate) + withdrawn = 0 + for t in _transactions: + if t.amount < 0: + withdrawn = 1 + break + if withdrawn == 1: + return Account.daily_interest(amount, 0.1, days) + else: + return Account.daily_interest(amount, 5, days) + else: + return Account.daily_interest(amount, 0.1, days) + def sumTransactions(self, checkAllTransactions=True): - return sum([t.amount for t in self.transactions]) \ No newline at end of file + """ + + :param checkAllTransactions: + :return: Return the sum of all transaction including debits and credits + """ + return sum([t.amount for t in self.transactions]) + + def getTransaction(self, startDate, endDate): + """ + Get the Customer Transaction between any time frame + + :param startDate: Start Date + :param endDate: End Date + :return: List of Transaction between the start date and end date + """ + transaction_data = [] + try: + start_date = DateUtils.toDate(startDate) + end_date = DateUtils.toDate(endDate) + except ValueError: + return ValueError("Incorrect date format") + for transaction in self.transactions: + if DateUtils.isDateWithinRange(transaction.transactionDate, startDate, endDate): + transaction_data.append(transaction) + return transaction_data + + @staticmethod + def daily_interest(P, r, days): + """ + Calculate the Daily/Continuous Interest + + :param P: Prinicipal Amount + :param r: Rate of Interest + :param days: No. of days upto which the interest is calculated + :return: Final amount with Interest accrued daily + """ + I = P + for x in range(days): + I *= 1 + ((r / 100.0) / 365.0) + return round(I, 2) + diff --git a/abcbank/bank.py b/abcbank/bank.py index 44711fe..c644eda 100644 --- a/abcbank/bank.py +++ b/abcbank/bank.py @@ -3,23 +3,38 @@ def __init__(self): self.customers = [] def addCustomer(self, customer): + """ + Add a customer to the bank + + :param customer: Customer Name + :return: None + """ 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 Interest Paid for all customer for all acounts the customer possess + + :return: interest payout + """ 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..5faf69c 100644 --- a/abcbank/customer.py +++ b/abcbank/customer.py @@ -1,30 +1,66 @@ -from account import CHECKING, SAVINGS, MAXI_SAVINGS +from account import CHECKING, SAVINGS, MAXI_SAVINGS, Account +from transaction import Transaction class Customer: def __init__(self, name): self.name = name - self.accounts = [] + self.accounts = {} def openAccount(self, account): - self.accounts.append(account) + """ + Create/Open an account + + :param account: account type + :return: Customer object + """ + if isinstance(account, Account): + self.accounts[account.accountType] = account + elif isinstance(account, int): + self.accounts[account] = Account(account) return self + def transfer(self, fromAccount, toAccount, amount): + """ + Transfer money between the customer accounts + + :param fromAccount: Account Type + :param toAccount: Account Type + :param amount: Amount to be transfered + :return: status of the transfer + """ + if fromAccount not in self.accounts or toAccount not in self.accounts: + return "Invalid Account" + _from = self.accounts[fromAccount] + _to = self.accounts[toAccount] + if _from.availableBalance < 0 or amount > _from.availableBalance: + return "Account does not have sufficient balance" + _from.availableBalance -= amount + _to.availableBalance += amount + _from.transactions.append(Transaction(-amount)) + _to.transactions.append(Transaction(amount)) + return "Transfer Succeeded" + def numAccs(self): + """ + + :return: number of accounts the customer has opened + """ return len(self.accounts) def totalInterestEarned(self): - return sum([a.interestEarned() for a in self.accounts]) + return sum([self.accounts[accountType].interestEarned() for accountType in self.accounts.keys()]) # 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]) + totalAcrossAllAccounts = sum( + [self.accounts[accountType].sumTransactions() for accountType in self.accounts.keys()]) statement = "Statement for %s" % self.name - for account in self.accounts: - statement = statement + self.statementForAccount(account) + for accountType in self.accounts.keys(): + statement += self.statementForAccount(self.accounts[accountType]) statement = statement + "\n\nTotal In All Accounts " + _toDollars(totalAcrossAllAccounts) return statement diff --git a/abcbank/date_provider.py b/abcbank/date_provider.py index 33b64eb..0b7de59 100644 --- a/abcbank/date_provider.py +++ b/abcbank/date_provider.py @@ -1,7 +1,82 @@ -from datetime import datetime +import datetime -class DateProvider: +class DateUtils: @staticmethod def now(): - return datetime.now() \ No newline at end of file + """ + + :return: Current date time + """ + return datetime.datetime.now() + + @staticmethod + def add(date, days): + """ + Add x days to the date + + :param date: date in string format + :param days: number of days to be added + :return: date in string format + """ + return (DateUtils.toDate(date) + datetime.timedelta(days=days)).strftime("%m/%d/%Y") + + @staticmethod + def toDate(date_str): + """ + Convert the date in string format to datetime format + + :param date_str: date in string format + :return: date in datetime format + """ + if isinstance(date_str,str): + return datetime.datetime.strptime(date_str, '%m/%d/%Y') + else: + return date_str + + @staticmethod + def find_no_days(date1_str, date2_str): + """ + Find no of days between 2 dates + + :param date1_str: date1 in string format + :param date2_str: date2 in string format + :return: no of day between 2 dates + """ + if isinstance(date1_str, str): + date1 = DateUtils.toDate(date1_str) + else: + date1 = date1_str + if isinstance(date2_str, str): + date2 = DateUtils.toDate(date2_str) + else: + date2 = date2_str + return (date2 - date1).days + + @staticmethod + def toString(date): + """ + Return datetime object in string format + + :param date: datetime object + :return: datetime in string + """ + return date.strftime("%m/%d/%Y") + + @staticmethod + def isDateWithinRange(date, startDate, endDate): + """ + Checks if the given date is within the time frame + + :param date: + :param startDate: + :param endDate: + :return: boolean true/false + """ + if isinstance(date, str): + date = DateUtils.toDate(date) + if isinstance(startDate, str): + startDate = DateUtils.toDate(startDate) + if isinstance(endDate, str): + endDate = DateUtils.toDate(endDate) + return DateUtils.find_no_days(date, startDate) <= 0 <= DateUtils.find_no_days(date, endDate) diff --git a/abcbank/transaction.py b/abcbank/transaction.py index 8e5b5ad..9424484 100644 --- a/abcbank/transaction.py +++ b/abcbank/transaction.py @@ -1,7 +1,20 @@ from datetime import datetime +from date_provider import DateUtils class Transaction: - def __init__(self, amount): + def __init__(self, amount, date=None): self.amount = amount - self.transactionDate = datetime.now() \ No newline at end of file + if not date: + self.transactionDate = datetime.now() + else: + if isinstance(date, str): + self.transactionDate = DateUtils.toDate(date) + else: + self.transactionDate = date + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.amount == other.amount and DateUtils.toString(self.transactionDate) == DateUtils.toString( + other.transactionDate) + return False diff --git a/tests/account_tests.py b/tests/account_tests.py new file mode 100644 index 0000000..eba9c0f --- /dev/null +++ b/tests/account_tests.py @@ -0,0 +1,35 @@ +from nose.tools import assert_equal + +from abcbank.account import Account, CHECKING, MAXI_SAVINGS + + +def test_accountBalance(): + testAccount = Account(CHECKING) + testAccount.deposit(1000) + assert_equal(testAccount.availableBalance, 1000, "Test available Balance") + testAccount.withdraw(500) + assert_equal(testAccount.availableBalance, 500, "Test available Balance after withdraw") + + +def test_checkDateFormat(): + assert_equal(Account(CHECKING).getTransaction("01-01-2016", "01-01-2016").message, "Incorrect date format") + + +def test_checkNoOfTransaction(): + test_acc = Account(CHECKING) + test_acc.deposit(500, "01/01/2016") + test_acc.deposit(500, "01/02/2016") + test_acc.deposit(500, "01/03/2016") + test_acc.deposit(500, "01/04/2016") + test_acc.deposit(500, "01/05/2016") + test_acc.deposit(500, "01/06/2016") + test_acc.deposit(500, "01/07/2016") + assert_equal(len(test_acc.getTransaction("01/02/2016", "01/04/2016")), 3) + + +def test_interest_after_x_days(): + test_acc = Account(MAXI_SAVINGS) + test_acc.deposit(3000, "01/01/2016") + assert_equal(test_acc.interest_earned_after_x_days(1), 3000.41) + assert_equal(test_acc.interest_earned_after_x_days(2), 3000.82) + assert_equal(test_acc.interest_earned_after_x_days(3), 3001.23) diff --git a/tests/bank_tests.py b/tests/bank_tests.py index 6de98db..a9566c1 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,6 @@ 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(), 150.0) + checkingAccount.withdraw(1000.0) + assert_equals(bank.totalInterestPaid(), 2.0) diff --git a/tests/customer_tests.py b/tests/customer_tests.py index 0211a4f..c16ba68 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, MAXI_SAVINGS +from abcbank.customer import Customer def test_statement(): @@ -29,8 +29,18 @@ 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 +def test_transfer(): + bob = Customer("Bob").openAccount(CHECKING) + bob.openAccount(SAVINGS) + bob.accounts[CHECKING].deposit(500) + + status = bob.transfer(CHECKING, SAVINGS, 100) + assert_equals(status, "Transfer Succeeded", "Checking status of transfer") + assert_equals(bob.accounts[CHECKING].availableBalance, 400, "Checking balance after transfer") + assert_equals(bob.accounts[SAVINGS].availableBalance, 100, "Checking balance after transfer") + + bob.accounts[SAVINGS].deposit(400) + assert_equals(bob.accounts[SAVINGS].availableBalance, 500, "Checking balance after deposit") + + status = bob.transfer(MAXI_SAVINGS, CHECKING, 100) + assert_equals(status, "Invalid Account") diff --git a/tests/dateprovider_test.py b/tests/dateprovider_test.py new file mode 100644 index 0000000..5e66810 --- /dev/null +++ b/tests/dateprovider_test.py @@ -0,0 +1,41 @@ +from nose.tools import assert_equals + +from abcbank.date_provider import DateUtils + + +def test_findnodays(): + date1 = "01/01/2016" + date2 = "01/01/2017" + assert_equals(DateUtils.find_no_days(date1, date2), 366) + + +def test_add_date(): + date1 = "01/01/2016" + assert_equals(DateUtils.add(date1, 5), "01/06/2016") + + +def test_isdateWithinRange(): + date = "01/04/2016" + startDate = "01/01/2016" + endDate = "01/06/2016" + assert_equals(DateUtils.isDateWithinRange(date, startDate, endDate), True) + + date = "01/04/2015" + startDate = "01/01/2016" + endDate = "01/06/2016" + assert_equals(DateUtils.isDateWithinRange(date, startDate, endDate), False) + + date = "01/04/2017" + startDate = "01/01/2016" + endDate = "01/06/2016" + assert_equals(DateUtils.isDateWithinRange(date, startDate, endDate), False) + + date = "01/01/2016" + startDate = "01/01/2016" + endDate = "01/06/2016" + assert_equals(DateUtils.isDateWithinRange(date, startDate, endDate), True) + + date = "01/06/2016" + startDate = "01/01/2016" + endDate = "01/06/2016" + assert_equals(DateUtils.isDateWithinRange(date, startDate, endDate), True) diff --git a/tests/transaction_tests.py b/tests/transaction_tests.py index 62caa8a..7a5c242 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 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_TransactionEquality(): + T1 = Transaction(100, "01/01/2016") + T2 = Transaction(100, "01/01/2016") + assert_equal(T1, T2)