Design a Ledger Co.

Problem

You work at a startup called The Ledger Co., a marketplace for banks to lend money to borrowers and receive payments for the loans.

The interest for the loan is calculated by I = PNR where P is the principal amount, N is the number of years and R is the rate of interest. The total amount to repay will be A = P + I

The amount should be paid back monthly in the form of EMIs. The borrowers can also pay a lump sum (that is, an amount more than their monthly EMI). In such a case, the lump sum will be deducted from the total amount (A) which can reduce the number of EMIs. This doesn’t affect the EMI amount unless the remaining total amount is less than the EMI. All transactions happen through The Ledger Co.

Your program should take as input:

  1. The bank name, borrower name, principal, interest and term.
  2. Lump sum payments if any.
  3. Given the bank name, borrower name, and EMI number, the program should print the total amount paid by the user (including the EMI number mentioned) and the remaining number of EMIs.

The output should be

  1. Amount paid so far, and number of EMIs remaining for the user with the bank

You need to design a system to find out how much amount a user has paid the bank and how many EMIs are remaining.

Input Commands

There are 3 input commands defined to separate out the actions. Your input format will start with either of these commands i.e LOAN, PAYMENT, BALANCE

  • LOAN
    The LOAN command receives a Bank name, Borrower name, Principal Amount, No of Years of Loan period and the Rate of Interest along with it.
    Format -
    LOAN BANK_NAME BORROWER_NAME PRINCIPAL NO_OF_YEARS RATE_OF_INTEREST
    Example -
    LOAN IDIDI Dale 10000 5 4 means a loan amount of 10000 is paid to Dale by IDIDI for a tenure of 5 years at 4% rate of interest.

  • PAYMENT
    The PAYMENT command receives a Bank name, Borrower name, Lump sum amount and EMI number along with it. The EMI number indicates that the lump sum payment is done after that EMI.
    Format -
    PAYMENT BANK_NAME BORROWER_NAME LUMP_SUM_AMOUNT EMI_NO
    Example -
    PAYMENT MBI Dale 1000 5 means a lump sum payment of 1000 was done by Dale to MBI after 5 EMI payments.

  • BALANCE
    The BALANCE command receives a Bank name, Borrower name and a EMI number along with it. It prints the total amount paid by the borrower, including all the Lump Sum amounts paid including that EMI number, and the no of EMIs remaining.

    Input format -
    BALANCE BANK_NAME BORROWER_NAME EMI_NO
    Example -
    BALANCE MBI Harry 12 means - print the amount paid including 12th EMI, and EMIs remaining for user Harry against the lender MBI.

    Output format -
    BANK_NAME BORROWER_NAME AMOUNT_PAID NO_OF_EMIS_LEFT Example -
    MBI Harry 1250 43

Assumptions

  1. Repayments will be paid every month as EMIs until the total amount is recovered.
  2. Lump sum amounts can be paid at any point of time before the end of tenure.
  3. The EMI amount will be always ceiled to the nearest integer. For example 86.676767 can be ceiled to 87 and 100.10 to 101.
  4. The no of EMIs should be ceiled to the nearest whole number. For example 23.79 will be ceiled to 24 and 23.1 will also be ceiled to 24.
  5. If the last EMI is more than the remaining amount to pay, then it should be adjusted to match the amount remaining to pay. E.g. if the remaining amount to pay is 150 and your EMI is 160, then the last EMI amount paid should be 150.
  6. The total amount remaining at any EMI number should always include the EMIs paid and lump sum payments until that number. For example if there was a lump sum payment after EMI number 10, then the balance command for EMI number 10 should include the lump sum payment as well.

Sample test cases

SAMPLE INPUT-OUTPUT 1

INPUT:

1
2
3
4
5
6
LOAN IDIDI Dale 10000 5 4
LOAN MBI Harry 2000 2 2
BALANCE IDIDI Dale 5
BALANCE IDIDI Dale 40
BALANCE MBI Harry 12
BALANCE MBI Harry 0

OUTPUT:

1
2
3
4
IDIDI Dale 1000 55
IDIDI Dale 8000 20
MBI Harry 1044 12
MBI Harry 0 24

SAMPLE INPUT-OUTPUT 2

INPUT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
LOAN IDIDI Dale 5000 1 6
LOAN MBI Harry 10000 3 7
LOAN UON Shelly 15000 2 9
PAYMENT IDIDI Dale 1000 5
PAYMENT MBI Harry 5000 10
PAYMENT UON Shelly 7000 12
BALANCE IDIDI Dale 3
BALANCE IDIDI Dale 6
BALANCE UON Shelly 12
BALANCE MBI Harry 12

OUTPUT:

1
2
3
4
IDIDI Dale 1326 9
IDIDI Dale 3652 4
UON Shelly 15856 3
MBI Harry 9044 10

Problem Link

Solution

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import sys
import math


class Borrower:
    def __init__(self, name):
        self.__name = name


class Bank:
    def __init__(self, name):
        self.__name = name


class Loan:
    def __init__(self, bank, borrower, principal, rate_of_interest, years):
        self.__borrower = borrower
        self.__bank = bank
        self.__principal = principal
        self.__years = years
        self.__roi = rate_of_interest
        self.__lump_sum = {}

    def add_lump_sum(self, lump_sum, emi_no):
        if emi_no in self.__lump_sum:
            self.__lump_sum[emi_no] += lump_sum
        else:
            self.__lump_sum[emi_no] = lump_sum

    def get_total_amount(self):
        return self.__principal + math.ceil(
            (self.__principal * self.__years * self.__roi) / 100
        )

    def get_emi_amount(self):
        return math.ceil(self.get_total_amount() / (12 * self.__years))

    def get_amount_paid(self, emi_no):
        total_amount = self.get_total_amount()
        emi_amount_paid = self.get_emi_amount() * emi_no
        lump_sum_paid = 0
        for ec, ls in self.__lump_sum.items():
            if ec <= emi_no:
                lump_sum_paid += ls

        return min(
            total_amount, emi_amount_paid + lump_sum_paid
        )  # if total_amount is min, difference should not be more than emi_amount, not checking this

    def get_remaining_emis(self, emi_no):
        amount_paid = self.get_amount_paid(emi_no)
        total_amount = self.get_total_amount()
        remaining_amount = total_amount - amount_paid
        emi_amount = self.get_emi_amount()

        return math.ceil(remaining_amount / emi_amount)


class LedgerCo:
    def __init__(self):
        self.__loans = {}
        self.__banks = {}
        self.__borrowers = {}

    def __get_loan_key(self, bank_name, borrower_name):
        return f"{bank_name}_{borrower_name}"

    def loan(self, bank_name, borrower_name, principal, term, interest):
        bank = self.__banks.get(bank_name, Bank(bank_name))
        borrower = self.__borrowers.get(borrower_name, Borrower(borrower_name))

        key = self.__get_loan_key(bank_name, borrower_name)
        if key in self.__loans:
            raise Exception("ongoing loan exists")

        loan = Loan(bank, borrower, principal, interest, term)
        self.__loans[key] = loan

    def payment(self, bank_name, borrower_name, lump_sum_amount, emi_no):
        key = self.__get_loan_key(bank_name, borrower_name)
        if key not in self.__loans:
            raise Exception("no loan exists")

        loan = self.__loans[key]
        loan.add_lump_sum(lump_sum_amount, emi_no)

    def balance(self, bank_name, borrower_name, emi_no):
        key = self.__get_loan_key(bank_name, borrower_name)
        if key not in self.__loans:
            raise Exception("no loan exists")

        loan = self.__loans[key]
        return (
            bank_name,
            borrower_name,
            loan.get_amount_paid(emi_no),
            loan.get_remaining_emis(emi_no),
        )


def main():
    ledger_co = LedgerCo()

    input_file = sys.argv[1]
    with open(input_file) as f:
        while True:
            line = f.readline()
            if not line:
                break

            command = line.strip().split()

            try:
                if command[0] == "LOAN":
                    ledger_co.loan(
                        command[1],
                        command[2],
                        float(command[3]),
                        float(command[4]),
                        float(command[5]),
                    )
                elif command[0] == "PAYMENT":
                    ledger_co.payment(
                        command[1], command[2], float(command[3]), float(command[4])
                    )
                elif command[0] == "BALANCE":
                    (
                        bank_name,
                        borrower_name,
                        amount_paid,
                        rem_emis,
                    ) = ledger_co.balance(command[1], command[2], float(command[3]))
                    print(
                        f"{bank_name} {borrower_name} {int(amount_paid)} {int(rem_emis)}"
                    )
                else:
                    raise Exception("invalid command")
            except Exception:
                raise Exception("invalid syntax")


if __name__ == "__main__":
    main()