Search code examples
sqlsql-servert-sqlfinance

Can a SQL query compute a loan account statement with interest and principal balances?


I have a table with a loanee's loan payments. This table has the date the loan was granted, the Amount Granted, Interest Rate on that loan (monthly) and the Dates and Amount paid in each installment.

Loanees do not always pay the same amount, nor do they pay the same day of the month. So the way my company manages interests due, is by calculating the difference in days between payment dates (with the exclusion of the first installment, where the difference in days comes from the date granted and the payment's date). Below, an excerpt from a made up loanee:

Payment_Number Date_granted Amount_Granted Interest_Rate Payment_Date Payment_Amount
1 2021-01-01 100000 0.03 2021-01-11 1,100.00
2 2021-01-01 100000 0.03 2021-01-31 2,000.00
3 2021-01-01 100000 0.03 2021-03-02 800.00

My problem is when I calculate the loan account statement. For the first row I know that Accrued Interests is:

Amount_Granted * (Interest Rate / 30) * (Payment_Date - Date_Granted)

However, I can't do the same for all other installments because, if part of the first one goes to Principal, the new Accrued Interests must be calculated with this new Principal instead of the Amount_Granted like this:

Payment_Number Date_granted Amount_Granted Interest_Rate Payment_Date Payment_Amount Accrued_interests Interests_Paid Principal_Paid New_Principal
1 2021-01-01 100000 0.03 2021-01-11 1,100.00 1,000.00 1,000.00 100.00 99,900.00
2 2021-01-01 100000 0.03 2021-01-31 2,000.00 1,998.00 1,998.00 2.00 99,898.00
3 2021-01-01 100000 0.03 2021-03-02 800.00 2,996.94 800.00 0.00 99,898.00

In Excel, this is a no-brainer. But I'm struggling to do this in SQL. So far I have tried doing a CASE WHEN, as an example:

SELECT 
    *,
    CASE 
       WHEN Payment_Number = 1 
           THEN (Amount_Granted * (Interest_Rate / 30) * (Payment_Date - Date_Granted)) 
           ELSE (New_Principal * (Interest_Rate / 30) * (LAG(Payment_Date,1) - Payment_Date)) 
    END AS Accrued_Interests,
    CASE 
        WHEN Accrued_Interests < Payment_Amount 
            THEN Accrued_Interests 
            ELSE Payment_Amount 
    END AS Interests_Paid,
    CASE 
        WHEN Accrued_Interests < Payment_Amount 
            THEN 0 
            ELSE Payment_Amount - Interests_Paid 
    END AS Principal_Paid,
    CASE 
        WHEN Payment_Number = 1 
            THEN Amount_Granted - Principal_Paid 
            ELSE New_Principal - LAG(New_Principal,1) - Principal_Paid 
    END AS New_Principal
FROM 
    MY_TABLE

But this yields an error because New_Principal has not yet been computed when I'm trying to use it in Accrued Interests. In SQLFiddle I get "Unknown column 'New_Principal' in 'field list'". I also tried with JOINS but no success, mostly because I have little experience with SQL to know how to do this.

Please, I kindly ask for your orientation. Below, the SQLFiddle Link for a reproduceable example:

SQLFiddle table

And the SQLFiddle Link for the error:

SQLFiddle error

PS: I know the LAG function is not complete, I just wanted to simplify things. Also: accrued interests that are not paid in complete go to an interests balance, but for the sake of simplification I am ignoring them.


Solution

  • The numbers in your example are a bit off and don't really follow the formula you specified. I tweaked the numbers to demonstrate how SQL Server can perform the calculations according to your algorithm.

    You can use a Recursive CTE to compute each payment according to the dates and updated principal from the previous payment, as in:

    with
    p as (
      select *,
        payment_amount - interest_paid as principal_paid,
        amount_granted - (payment_amount - interest_paid) as new_principal, cast(null as float) as prev_principal
      from (
        select *,
          case when payment_amount < accrued_interest then payment_amount else accrued_interest end as interest_paid
        from (
          select
            Payment_Number, Date_granted, Amount_Granted,
            Interest_Rate, Payment_Date, Payment_Amount,
            Amount_Granted * (Interest_Rate / 30) * datediff(day, date_granted, payment_date) as accrued_interest
          from account where payment_number = 1
        ) x
      ) y
     union all
      select
        Payment_Number, Date_granted, Amount_Granted,
        Interest_Rate, Payment_Date, Payment_Amount,
        accrued_interest, interest_paid,
        payment_amount - interest_paid,
        prev_principal - (payment_amount - interest_paid),
        prev_principal
      from (
        select *,
          case when payment_amount < accrued_interest then payment_amount else accrued_interest end as interest_paid
        from (
          select a.Payment_Number, a.Date_granted, a.Amount_Granted,
            a.Interest_Rate, a.Payment_Date, a.Payment_Amount,
            p.new_principal * (a.Interest_Rate / 30) * datediff(day, a.date_granted, a.payment_date) as accrued_interest,
            p.new_principal as prev_principal
          from p
          join account a on a.payment_number = p.payment_number + 1
        ) z
      ) w
    )
    select * from p;
    

    Result:

    Payment_Number  Date_granted  Amount_Granted  Interest_Rate  Payment_Date  Payment_Amount  accrued_interest  interest_paid  principal_paid  new_principal  prev_principal
    --------------  ------------  --------------  -------------  ------------  --------------  ----------------  -------------  --------------  -------------  --------------
                 1   2021-01-01          100,000           0.03   2021-01-11            1,100             1,000          1,000             100         99,900          <null>
                 2   2021-01-01          100,000           0.03   2021-01-31            3,000             2,997          2,997               3         99,897          99,900
                 3   2021-01-01          100,000           0.03   2021-03-02            2,800          5,993.82          2,800               0         99,897          99,897
    

    See running example at db<>fiddle.