FINERACT-2326: Add the ability to reschedule non-interest-bearing progressive loans to have interest

This commit is contained in:
mark.vituska
2025-08-04 14:03:00 +02:00
committed by Adam Saghy
parent 6a67e58a3a
commit 3428e33035
14 changed files with 171 additions and 271 deletions
@@ -75,8 +75,7 @@ public class LoanRescheduleStepDef extends AbstractStepDef {
: Integer.valueOf(rescheduleData.get(4)); : Integer.valueOf(rescheduleData.get(4));
Integer extraTerms = (rescheduleData.get(5) == null || "0".equals(rescheduleData.get(5))) ? null Integer extraTerms = (rescheduleData.get(5) == null || "0".equals(rescheduleData.get(5))) ? null
: Integer.valueOf(rescheduleData.get(5)); : Integer.valueOf(rescheduleData.get(5));
BigDecimal newInterestRate = (rescheduleData.get(6) == null || "0".equals(rescheduleData.get(6))) ? null BigDecimal newInterestRate = (rescheduleData.get(6) == null) ? null : new BigDecimal(rescheduleData.get(6));
: new BigDecimal(rescheduleData.get(6));
PostCreateRescheduleLoansRequest request = new PostCreateRescheduleLoansRequest()// PostCreateRescheduleLoansRequest request = new PostCreateRescheduleLoansRequest()//
.loanId(loanId)// .loanId(loanId)//
@@ -6708,8 +6708,8 @@ Feature: Loan
| 2 | 15 | 29 March 2024 | 24 March 2024 | 243.79 | 121.89 | 0.0 | 0.0 | 0.0 | 121.89 | 121.89 | 121.89 | 0.0 | 0.0 | | 2 | 15 | 29 March 2024 | 24 March 2024 | 243.79 | 121.89 | 0.0 | 0.0 | 0.0 | 121.89 | 121.89 | 121.89 | 0.0 | 0.0 |
| | | 01 April 2024 | | 312.69 | | | 0.0 | | 0.0 | 0.0 | | | | | | | 01 April 2024 | | 312.69 | | | 0.0 | | 0.0 | 0.0 | | | |
| 3 | 0 | 01 April 2024 | 01 April 2024 | 478.31 | 78.17 | 0.0 | 0.0 | 0.0 | 78.17 | 78.17 | 0.0 | 0.0 | 0.0 | | 3 | 0 | 01 April 2024 | 01 April 2024 | 478.31 | 78.17 | 0.0 | 0.0 | 0.0 | 78.17 | 78.17 | 0.0 | 0.0 | 0.0 |
| 4 | 15 | 13 April 2024 | | 239.16 | 239.15 | 0.0 | 0.0 | 0.0 | 239.15 | 121.89 | 121.89 | 0.0 | 117.26 | | 4 | 15 | 13 April 2024 | | 239.15 | 239.16 | 0.0 | 0.0 | 0.0 | 239.16 | 121.89 | 121.89 | 0.0 | 117.27 |
| 5 | 15 | 28 April 2024 | | 0.0 | 239.16 | 0.0 | 0.0 | 0.0 | 239.16 | 121.9 | 121.9 | 0.0 | 117.26 | | 5 | 15 | 28 April 2024 | | 0.0 | 239.15 | 0.0 | 0.0 | 0.0 | 239.15 | 121.9 | 121.9 | 0.0 | 117.25 |
Then Loan Repayment schedule has the following data in Total row: Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 800.27 | 0.0 | 0.0 | 0.0 | 800.27 | 565.75 | 365.68 | 0.0 | 234.52 | | 800.27 | 0.0 | 0.0 | 0.0 | 800.27 | 565.75 | 365.68 | 0.0 | 234.52 |
@@ -6733,8 +6733,8 @@ Feature: Loan
| 2 | 15 | 29 March 2024 | 24 March 2024 | 243.79 | 121.89 | 0.0 | 0.0 | 0.0 | 121.89 | 121.89 | 121.89 | 0.0 | 0.0 | | 2 | 15 | 29 March 2024 | 24 March 2024 | 243.79 | 121.89 | 0.0 | 0.0 | 0.0 | 121.89 | 121.89 | 121.89 | 0.0 | 0.0 |
| | | 01 April 2024 | | 312.69 | | | 0.0 | | 0.0 | 0.0 | | | | | | | 01 April 2024 | | 312.69 | | | 0.0 | | 0.0 | 0.0 | | | |
| 3 | 0 | 01 April 2024 | 01 April 2024 | 478.31 | 78.17 | 0.0 | 0.0 | 0.0 | 78.17 | 78.17 | 0.0 | 0.0 | 0.0 | | 3 | 0 | 01 April 2024 | 01 April 2024 | 478.31 | 78.17 | 0.0 | 0.0 | 0.0 | 78.17 | 78.17 | 0.0 | 0.0 | 0.0 |
| 4 | 15 | 13 April 2024 | 10 April 2024 | 239.16 | 239.15 | 0.0 | 0.0 | 0.0 | 239.15 | 239.15 | 239.15 | 0.0 | 0.0 | | 4 | 15 | 13 April 2024 | 10 April 2024 | 239.15 | 239.16 | 0.0 | 0.0 | 0.0 | 239.16 | 239.16 | 239.16 | 0.0 | 0.0 |
| 5 | 15 | 28 April 2024 | 10 April 2024 | 0.0 | 239.16 | 0.0 | 0.0 | 0.0 | 239.16 | 239.16 | 239.16 | 0.0 | 0.0 | | 5 | 15 | 28 April 2024 | 10 April 2024 | 0.0 | 239.15 | 0.0 | 0.0 | 0.0 | 239.15 | 239.15 | 239.15 | 0.0 | 0.0 |
Then Loan Repayment schedule has the following data in Total row: Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 800.27 | 0.0 | 0.0 | 0.0 | 800.27 | 800.27 | 600.2 | 0.0 | 0.0 | | 800.27 | 0.0 | 0.0 | 0.0 | 800.27 | 800.27 | 600.2 | 0.0 | 0.0 |
@@ -918,140 +918,91 @@ Feature: LoanReschedule
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
@TestRailId:C3809 @TestRailId:C3882
Scenario: Verify change interest rate with 0->4 reschedule scenario - UC1 Scenario: Reschedule progressive loan to 0 percent interest then back to 10 percent
When Admin sets the business date to "01 April 2025" When Admin sets the business date to "1 January 2024"
When Admin creates a client with random data When Admin creates a client with random data
When Admin creates a fully customized loan with the following data: When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 April 2025 | 1000 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30 | 1 January 2024 | 100 | 10 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 April 2025" with "1000" amount and expected disbursement date on "01 April 2025" And Admin successfully approves the loan on "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024"
When Admin successfully disburse the loan on "01 April 2025" with "1000" EUR transaction amount When Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 4 periods, with the following data for periods: Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 April 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 May 2025 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | | 1 | 31 | 01 February 2024 | | 83.67 | 16.33 | 0.83 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 |
| 2 | 31 | 01 June 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | | 2 | 29 | 01 March 2024 | | 67.21 | 16.46 | 0.7 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 |
| 3 | 30 | 01 July 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | | 3 | 31 | 01 April 2024 | | 50.61 | 16.6 | 0.56 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 |
| 4 | 31 | 01 August 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | | 4 | 30 | 01 May 2024 | | 33.87 | 16.74 | 0.42 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 |
| 5 | 31 | 01 June 2024 | | 16.99 | 16.88 | 0.28 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.99 | 0.14 | 0.0 | 0.0 | 17.13 | 0.0 | 0.0 | 0.0 | 17.13 |
Then Loan Repayment schedule has the following data in Total row: Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | | 100.0 | 2.93 | 0.0 | 0.0 | 102.93 | 0.0 | 0.0 | 0.0 | 102.93 |
Then Loan Transactions tab has the following data: When Admin creates and approves Loan reschedule with the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 April 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | | 02 January 2024 | 01 January 2024 | | | | | 0 |
When Admin sets the business date to "01 May 2025" Then Loan Repayment schedule has 6 periods, with the following data for periods:
And Admin runs inline COB job for Loan | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
And Customer makes "AUTOPAY" repayment on "01 May 2025" with 250.0 EUR transaction amount | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
Then Loan Repayment schedule has 4 periods, with the following data for periods: | 1 | 31 | 01 February 2024 | | 83.33 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 2 | 29 | 01 March 2024 | | 66.66 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| | | 01 April 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | | 3 | 31 | 01 April 2024 | | 49.99 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| 1 | 30 | 01 May 2025 | 01 May 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | | 4 | 30 | 01 May 2024 | | 33.32 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| 2 | 31 | 01 June 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | | 5 | 31 | 01 June 2024 | | 16.65 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| 3 | 30 | 01 July 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | | 6 | 30 | 01 July 2024 | | 0.0 | 16.65 | 0.0 | 0.0 | 0.0 | 16.65 | 0.0 | 0.0 | 0.0 | 16.65 |
| 4 | 31 | 01 August 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
Then Loan Repayment schedule has the following data in Total row: Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 250.0 | 0.0 | 0.0 | 750.0 | | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
Then Loan Transactions tab has the following data: When Admin creates and approves Loan reschedule with the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 April 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | | 03 January 2024 | 01 January 2024 | | | | | 10 |
| 01 May 2025 | Repayment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 750.0 | false | false | Then Loan Repayment schedule has 6 periods, with the following data for periods:
When Admin sets the business date to "05 May 2025" | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
And Admin runs inline COB job for Loan | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
# --- Loan reschedule: Interest rate modification effective from next day --- | 1 | 31 | 01 February 2024 | | 83.66 | 16.34 | 0.81 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 |
#When Admin attempt Loan reschedule with changing interest rate to zero following data validation error should occur | 2 | 29 | 01 March 2024 | | 67.21 | 16.45 | 0.7 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 |
Then Loan reschedule with the following data results a 400 error and "LOAN_RESCHEDULE_NOT_ALLOWED_FROM_ZERO_TO_NEW_INTEREST_RATE" error message | 3 | 31 | 01 April 2024 | | 50.62 | 16.59 | 0.56 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 |
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate | | 4 | 30 | 01 May 2024 | | 33.89 | 16.73 | 0.42 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 |
| 06 May 2025 | 05 May 2025 | | | | | 4 | | 5 | 31 | 01 June 2024 | | 17.02 | 16.87 | 0.28 | 0.0 | 0.0 | 17.15 | 0.0 | 0.0 | 0.0 | 17.15 |
When Admin sets the business date to "06 May 2025" | 6 | 30 | 01 July 2024 | | 0.0 | 17.02 | 0.14 | 0.0 | 0.0 | 17.16 | 0.0 | 0.0 | 0.0 | 17.16 |
And Customer makes "AUTOPAY" repayment on "06 May 2025" with 100.0 EUR transaction amount Then Loan Repayment schedule has the following data in Total row:
# TODO uncomment and update repayment schedule after fixing PS-2665 | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
# Then Loan Repayment schedule has 4 periods, with the following data for periods: | 100.0 | 2.91 | 0.0 | 0.0 | 102.91 | 0.0 | 0.0 | 0.0 | 102.91 |
# | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
# | | | 01 April 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
# | 1 | 30 | 01 May 2025 | 01 May 2025 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
# | 2 | 31 | 01 June 2025 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 100.0 | 100.0 | 0.0 | 150.0 |
# | 3 | 30 | 01 July 2025 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
# | 4 | 31 | 01 August 2025 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
# Then Loan Repayment schedule has the following data in Total row:
# | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
# | 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 350.0 | 100.0 | 0.0 | 650.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 April 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 01 May 2025 | Repayment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 750.0 | false | false |
| 06 May 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 650.0 | false | false |
Then Loan status will be "ACTIVE"
@TestRailId:C3810 @TestRailId:C3883
Scenario: Verify change interest rate with 4->0 reschedule scenario - UC2 Scenario: Create progressive loan with 0 percent interest then to 10 percent after first period
When Admin sets the business date to "01 April 2025" When Admin sets the business date to "1 January 2024"
When Admin creates a client with random data When Admin creates a client with random data
When Admin creates a fully customized loan with the following data: When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 April 2025 | 1000 | 4 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30 | 1 January 2024 | 100 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 April 2025" with "1000" amount and expected disbursement date on "01 April 2025" And Admin successfully approves the loan on "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024"
When Admin successfully disburse the loan on "01 April 2025" with "1000" EUR transaction amount When Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 4 periods, with the following data for periods: Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 April 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 30 | 01 May 2025 | | 751.24 | 248.76 | 3.33 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | | 1 | 31 | 01 February 2024 | | 83.33 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| 2 | 31 | 01 June 2025 | | 501.65 | 249.59 | 2.5 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | | 2 | 29 | 01 March 2024 | | 66.66 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| 3 | 30 | 01 July 2025 | | 251.23 | 250.42 | 1.67 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | | 3 | 31 | 01 April 2024 | | 49.99 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| 4 | 31 | 01 August 2025 | | 0.0 | 251.23 | 0.84 | 0.0 | 0.0 | 252.07 | 0.0 | 0.0 | 0.0 | 252.07 | | 4 | 30 | 01 May 2024 | | 33.32 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| 5 | 31 | 01 June 2024 | | 16.65 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.65 | 0.0 | 0.0 | 0.0 | 16.65 | 0.0 | 0.0 | 0.0 | 16.65 |
Then Loan Repayment schedule has the following data in Total row: Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 8.34 | 0.0 | 0.0 | 1008.34 | 0.0 | 0.0 | 0.0 | 1008.34 | | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
Then Loan Transactions tab has the following data: When Admin creates and approves Loan reschedule with the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 01 April 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | | 02 February 2024 | 01 January 2024 | | | | | 10 |
When Admin sets the business date to "01 May 2025" Then Loan Repayment schedule has 6 periods, with the following data for periods:
And Admin runs inline COB job for Loan | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
And Customer makes "AUTOPAY" repayment on "01 May 2025" with 252.09 EUR transaction amount | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
Then Loan Repayment schedule has 4 periods, with the following data for periods: | 1 | 31 | 01 February 2024 | | 83.33 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 | 0.0 | 0.0 | 0.0 | 16.67 |
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | 2 | 29 | 01 March 2024 | | 66.94 | 16.39 | 0.69 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 |
| | | 01 April 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | | | 3 | 31 | 01 April 2024 | | 50.42 | 16.52 | 0.56 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 |
| 1 | 30 | 01 May 2025 | 01 May 2025 | 751.24 | 248.76 | 3.33 | 0.0 | 0.0 | 252.09 | 252.09 | 0.0 | 0.0 | 0.0 | | 4 | 30 | 01 May 2024 | | 33.76 | 16.66 | 0.42 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 |
| 2 | 31 | 01 June 2025 | | 501.65 | 249.59 | 2.5 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | | 5 | 31 | 01 June 2024 | | 16.96 | 16.8 | 0.28 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 |
| 3 | 30 | 01 July 2025 | | 251.23 | 250.42 | 1.67 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 | | 6 | 30 | 01 July 2024 | | 0.0 | 16.96 | 0.14 | 0.0 | 0.0 | 17.1 | 0.0 | 0.0 | 0.0 | 17.1 |
| 4 | 31 | 01 August 2025 | | 0.0 | 251.23 | 0.84 | 0.0 | 0.0 | 252.07 | 0.0 | 0.0 | 0.0 | 252.07 |
Then Loan Repayment schedule has the following data in Total row: Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 8.34 | 0.0 | 0.0 | 1008.34 | 252.09 | 0.0 | 0.0 | 756.25 | | 100.0 | 2.09 | 0.0 | 0.0 | 102.09 | 0.0 | 0.0 | 0.0 | 102.09 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 April 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 30 April 2025 | Accrual | 3.22 | 0.0 | 3.22 | 0.0 | 0.0 | 0.0 | false | false |
| 01 May 2025 | Repayment | 252.09 | 248.76 | 3.33 | 0.0 | 0.0 | 751.24 | false | false |
When Admin sets the business date to "05 May 2025"
And Admin runs inline COB job for Loan
# --- Loan reschedule: Interest rate modification effective from next day ---
#When Admin attempt Loan reschedule with changing interest rate from zero to 4 following data validation error should occur
Then Loan reschedule with the following data results a 400 error and "LOAN_RESCHEDULE_NOT_ALLOWED_FROM_CURRENT_INTEREST_RATE_TO_ZERO" error message
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 06 May 2025 | 05 May 2025 | | | | | 0 |
When Admin sets the business date to "06 May 2025"
And Customer makes "AUTOPAY" repayment on "06 May 2025" with 100.0 EUR transaction amount
# TODO uncomment and update repayment schedule after fixing PS-2634
# Then Loan Repayment schedule has 4 periods, with the following data for periods:
# | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
# | | | 01 April 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
# | 1 | 30 | 01 May 2025 | 01 May 2025 | 751.24 | 248.76 | 3.33 | 0.0 | 0.0 | 252.09 | 252.09 | 0.0 | 0.0 | 0.0 |
# | 2 | 31 | 01 June 2025 | | 501.37 | 249.87 | 2.22 | 0.0 | 0.0 | 252.09 | 100.0 | 100.0 | 0.0 | 152.09 |
# | 3 | 30 | 01 July 2025 | | 250.95 | 250.42 | 1.67 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 |
# | 4 | 31 | 01 August 2025 | | 0.0 | 250.95 | 0.84 | 0.0 | 0.0 | 251.79 | 0.0 | 0.0 | 0.0 | 251.79 |
# Then Loan Repayment schedule has the following data in Total row:
# | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
# | 1000.0 | 8.06 | 0.0 | 0.0 | 1008.06 | 352.09 | 100.0 | 0.0 | 655.97 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 April 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 30 April 2025 | Accrual | 3.22 | 0.0 | 3.22 | 0.0 | 0.0 | 0.0 | false | false |
| 01 May 2025 | Repayment | 252.09 | 248.76 | 3.33 | 0.0 | 0.0 | 751.24 | false | false |
| 01 May 2025 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false |
| 02 May 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false |
| 03 May 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false |
| 04 May 2025 | Accrual | 0.08 | 0.0 | 0.08 | 0.0 | 0.0 | 0.0 | false | false |
| 06 May 2025 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 651.24 | false | false |
Then Loan status will be "ACTIVE"
@@ -507,7 +507,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder(); MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder();
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { && loanTransaction.getLoan().isInterestRecalculationEnabled()) {
var model = progressiveTransactionCtx.getModel(); var model = progressiveTransactionCtx.getModel();
// Copy and paste Logic from super.handleCreditBalanceRefund // Copy and paste Logic from super.handleCreditBalanceRefund
@@ -778,7 +778,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) { protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
if (loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { if (loanTransaction.getLoan().isInterestRecalculationEnabled()) {
processCreditTransactionWithEmiCalculator(loanTransaction, (ProgressiveTransactionCtx) ctx); processCreditTransactionWithEmiCalculator(loanTransaction, (ProgressiveTransactionCtx) ctx);
} else if (hasNoCustomCreditAllocationRule(loanTransaction)) { } else if (hasNoCustomCreditAllocationRule(loanTransaction)) {
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments()); super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
@@ -1224,8 +1224,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging // TODO: Fix this and enhance EMICalculator to support reamortization and reaging
final Loan loan = disbursementTransaction.getLoan(); if (shouldUseEmiCalculation(transactionCtx, disbursementTransaction.getTransactionDate())) {
if (loan.isInterestBearing()) {
handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx); handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx);
} else { } else {
handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx);
@@ -1356,7 +1355,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
private void handleCapitalizedIncome(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) { private void handleCapitalizedIncome(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
if (capitalizedIncomeTransaction.getLoan().isInterestBearing()) { // TODO: Fix this and enhance EMICalculator to support reamortization and reaging
if (shouldUseEmiCalculation(transactionCtx, capitalizedIncomeTransaction.getTransactionDate())) {
handleCapitalizedIncomeWithEMICalculator(capitalizedIncomeTransaction, transactionCtx); handleCapitalizedIncomeWithEMICalculator(capitalizedIncomeTransaction, transactionCtx);
} else { } else {
handleCapitalizedIncomeWithoutEMICalculator(capitalizedIncomeTransaction, transactionCtx); handleCapitalizedIncomeWithoutEMICalculator(capitalizedIncomeTransaction, transactionCtx);
@@ -1476,6 +1476,24 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
} }
private boolean shouldUseEmiCalculation(TransactionCtx transactionCtx, LocalDate transactionDate) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx) {
boolean hasActiveReAmortization = progressiveTransactionCtx.getAlreadyProcessedTransactions().stream()
.anyMatch(t -> t.getTypeOf().isReAmortize() && t.isNotReversed());
boolean hasActiveReAge = progressiveTransactionCtx.getAlreadyProcessedTransactions().stream()
.anyMatch(t -> t.getTypeOf().isReAge() && t.isNotReversed());
if (hasActiveReAmortization) {
return false;
} else {
return !hasActiveReAge || !DateUtils.isAfter(transactionDate, progressiveTransactionCtx.getModel().getMaturityDate());
}
}
// From now on we are defaulting to using the EMICalculator on all progressive loans. However currently the
// model is not aware of re-aging and re-amortization. So only these specific cases should ignore this
// requirement. This method can be removed once these operations are supported by the EMI model.
return true;
}
protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) { protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) {
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments()); super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
} }
@@ -24,7 +24,6 @@ import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.Loan
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateAndRetrieveRescheduleFromDate; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateAndRetrieveRescheduleFromDate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateApprovalDate; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateApprovalDate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateForOverdueCharges; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateForOverdueCharges;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateInterestRate;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateLoanIsActive; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateLoanIsActive;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonComment; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonComment;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonId; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonId;
@@ -78,8 +77,7 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder); validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder); validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder); LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
BigDecimal interestRate = validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper, BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
jsonElement, dataValidatorBuilder);
validateUnsupportedParams(jsonElement, dataValidatorBuilder); validateUnsupportedParams(jsonElement, dataValidatorBuilder);
boolean hasInterestRateChange = interestRate != null; boolean hasInterestRateChange = interestRate != null;
@@ -197,4 +195,13 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
"Interest rate change for the provided date is already exists.", rescheduleFromDate); "Interest rate change for the provided date is already exists.", rescheduleFromDate);
} }
} }
private BigDecimal validateInterestRate(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
final BigDecimal interestRate = fromJsonHelper
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull()
.zeroOrPositiveAmount();
return interestRate;
}
} }
@@ -182,7 +182,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
} }
private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction transaction) { private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction transaction) {
if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) { if (loan.isInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
reprocessLoanTransactionsService.reprocessTransactions(loan); reprocessLoanTransactionsService.reprocessTransactions(loan);
} else { } else {
reprocessLoanTransactionsService.processLatestTransaction(transaction, loan); reprocessLoanTransactionsService.processLatestTransaction(transaction, loan);
@@ -161,6 +161,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
} }
private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) { private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate()))
.forEach(rp -> rp.setTotalCapitalizedIncomeAmount(
MathUtil.nullToZero(rp.getTotalCapitalizedIncomeAmount()).add(operation.getAmount().getAmount())));
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(), scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(),
scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> { scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> {
calculateEMIValueAndRateFactors( calculateEMIValueAndRateFactors(
@@ -920,9 +924,11 @@ public final class ProgressiveEMICalculator implements EMICalculator {
// already repaid principals should be subtracted from total disbursed amount to calculate correct EMI. // already repaid principals should be subtracted from total disbursed amount to calculate correct EMI.
BigDecimal alreadyRepaidPrincipals = first.getPrevious() BigDecimal alreadyRepaidPrincipals = first.getPrevious()
.map(rp -> rp.calculateTotalDisbursedAmountTillGivenPeriod(null).subtract(rp.getOutstandingLoanBalance().getAmount())) .map(rp -> rp.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(null)
.subtract(rp.getOutstandingLoanBalance().getAmount()))
.orElse(BigDecimal.ZERO); .orElse(BigDecimal.ZERO);
Money total = Money.of(currency, first.calculateTotalDisbursedAmountTillGivenPeriod(first.getLastInterestPeriod())) Money total = Money
.of(currency, first.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(first.getLastInterestPeriod()))
.plus(sumOfInterest).minus(alreadyRepaidPrincipals); .plus(sumOfInterest).minus(alreadyRepaidPrincipals);
Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc); Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc);
@@ -150,7 +150,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
BigDecimal baseAmount = switch (method) { BigDecimal baseAmount = switch (method) {
case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAmountTillGivenPeriod(this)); case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(this));
case DECLINING_BALANCE -> getOutstandingLoanBalance().getAmount(); case DECLINING_BALANCE -> getOutstandingLoanBalance().getAmount();
default -> throw new UnsupportedOperationException("Method not implemented: " + method); default -> throw new UnsupportedOperationException("Method not implemented: " + method);
}; };
@@ -84,6 +84,10 @@ public final class RepaymentPeriod {
@Getter @Getter
private BigDecimal totalDisbursedAmount; private BigDecimal totalDisbursedAmount;
@Setter
@Getter
private BigDecimal totalCapitalizedIncomeAmount;
@JsonExclude @JsonExclude
@Getter @Getter
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail; private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
@@ -389,16 +393,21 @@ public final class RepaymentPeriod {
/** /**
* @param tillPeriod * @param tillPeriod
* can be null. if null it calculates total disbursement including last interest period. * can be null. if null it calculates total disbursement including last interest period.
* @return disbursed amount til interest period. * @return disbursed and capitalized income amount till interest period.
*/ */
public BigDecimal calculateTotalDisbursedAmountTillGivenPeriod(InterestPeriod tillPeriod) { public BigDecimal calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(InterestPeriod tillPeriod) {
BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount()); BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount()).add(MathUtil.nullToZero(getTotalCapitalizedIncomeAmount()));
for (InterestPeriod interestPeriod : this.interestPeriods) { for (InterestPeriod interestPeriod : this.interestPeriods) {
if (interestPeriod.equals(tillPeriod)) { if (interestPeriod.equals(tillPeriod)) {
break; break;
} }
if (!interestPeriod.getDueDate().equals(getFromDate()) && interestPeriod.getDisbursementAmount() != null) { if (!interestPeriod.getDueDate().equals(getFromDate())) {
res = res.add(interestPeriod.getDisbursementAmount().getAmount()); if (interestPeriod.getDisbursementAmount() != null) {
res = res.add(interestPeriod.getDisbursementAmount().getAmount());
}
if (interestPeriod.getCapitalizedIncomePrincipal() != null) {
res = res.add(interestPeriod.getCapitalizedIncomePrincipal().getAmount());
}
} }
} }
return res; return res;
@@ -535,7 +535,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Loan loan = mock(Loan.class); Loan loan = mock(Loan.class);
when(loan.getLoanRepaymentScheduleDetail()).thenReturn(loanProductRelatedDetail); when(loan.getLoanRepaymentScheduleDetail()).thenReturn(loanProductRelatedDetail);
when(loan.isInterestBearing()).thenReturn(true);
LoanRepaymentScheduleInstallment installment1 = spy( LoanRepaymentScheduleInstallment installment1 = spy(
new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(200.0), new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(200.0),
@@ -95,16 +95,18 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
AdvancedPaymentScheduleTransactionProcessor advancedProcessor, Loan loan, LoanTransaction loanTransaction) { AdvancedPaymentScheduleTransactionProcessor advancedProcessor, Loan loan, LoanTransaction loanTransaction) {
Optional<ProgressiveLoanInterestScheduleModel> savedModel = modelRepository.getSavedModel(loan, Optional<ProgressiveLoanInterestScheduleModel> savedModel = modelRepository.getSavedModel(loan,
loanTransaction.getTransactionDate()); loanTransaction.getTransactionDate());
ProgressiveLoanInterestScheduleModel model = savedModel
.orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate()));
ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(), ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
new ChangedTransactionDetail(), savedModel.orElse(null), getTotalRefundInterestAmount(loan)); new ChangedTransactionDetail(), model, getTotalRefundInterestAmount(loan));
progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan)); progressiveContext.getAlreadyProcessedTransactions().addAll(loanTransactionService.retrieveListOfTransactionsForReprocessing(loan));
progressiveContext.setChargedOff(loan.isChargedOff()); progressiveContext.setChargedOff(loan.isChargedOff());
progressiveContext.setContractTerminated(loan.isContractTermination()); progressiveContext.setContractTerminated(loan.isContractTermination());
ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext); ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext);
if (savedModel.isPresent() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { if (!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
modelRepository.writeInterestScheduleModel(loan, savedModel.get()); modelRepository.writeInterestScheduleModel(loan, model);
} }
return result; return result;
} }
@@ -124,7 +126,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor( final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(
transactionProcessingStrategyCode); transactionProcessingStrategyCode);
if (loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor if (loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { && loanTransaction.getLoan().isInterestRecalculationEnabled()) {
return processLatestTransactionProgressiveInterestRecalculation(advancedProcessor, loanTransaction.getLoan(), loanTransaction); return processLatestTransactionProgressiveInterestRecalculation(advancedProcessor, loanTransaction.getLoan(), loanTransaction);
} }
return loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, ctx); return loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, ctx);
@@ -36,8 +36,11 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -145,10 +148,25 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
@Override @Override
public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) { public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) {
final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService.processLatestTransaction( LoanRepaymentScheduleTransactionProcessor transactionProcessor = loanTransactionProcessingService
loan.getTransactionProcessingStrategyCode(), loanTransaction, .getTransactionProcessor(loan.getTransactionProcessingStrategyCode());
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail())); TransactionCtx transactionCtx;
if (transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor) {
Optional<ProgressiveLoanInterestScheduleModel> savedModel = interestScheduleModelRepositoryWrapper.getSavedModel(loan,
loanTransaction.getTransactionDate());
ProgressiveLoanInterestScheduleModel model = savedModel
.orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate()));
transactionCtx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), model);
} else {
transactionCtx = new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail());
}
final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService
.processLatestTransaction(loan.getTransactionProcessingStrategyCode(), loanTransaction, transactionCtx);
final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream() final List<LoanTransaction> newTransactions = changedTransactionDetail.getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList(); .map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList();
loan.getLoanTransactions().addAll(newTransactions); loan.getLoanTransactions().addAll(newTransactions);
@@ -6000,115 +6000,6 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
}); });
} }
// uc156: Avoid Loan Reschedule to modify Interest Rate from X value to Zero
// 1. Create a Loan product
// 2. Submit, Approve and Disburse Loan with Nominal Interest equal to 4%
// 3. Apply a Loan repayment
// 4. Try to create Loan Reschedule with new Interest Rate equal to zero to get the exception
@Test
public void uc156() {
final String operationDate = "1 April 2025";
AtomicLong createdLoanId = new AtomicLong();
runAt("1 April 2025", () -> {
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = create4IProgressive().interestRatePerPeriod(4.0).numberOfRepayments(4)//
.installmentAmountInMultiplesOf(null)//
.multiDisburseLoan(false)//
.disallowExpectedDisbursements(null)//
.allowApprovedDisbursedAmountsOverApplied(false)//
.overAppliedCalculationType(null)//
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
.overAppliedNumber(null)//
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(),
operationDate, 1000.0, 4.0, 4, null);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
createdLoanId.set(loanResponse.getLoanId());
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
.approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1000.0)));
});
runAt("1 May 2025", () -> {
executeInlineCOB(createdLoanId.get());
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
.transactionDate("1 May 2025").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(250.00));
});
runAt("6 May 2025", () -> {
executeInlineCOB(createdLoanId.get());
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
() -> loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
.loanId(createdLoanId.get()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("6 May 2025")
.newInterestRate(BigDecimal.ZERO).rescheduleReasonId(1L).rescheduleFromDate("1 June 2025")));
Assertions.assertTrue(
callFailedRuntimeException.getMessage().contains("The parameter `newInterestRate` must be greater than 0."));
});
}
// uc157: Avoid Loan Reschedule to modify Interest Rate from Zero to X value
// 1. Create a Loan product
// 2. Submit, Approve and Disburse Loan with Nominal Interest equal to 0 (zero)
// 3. Apply a Loan repayment
// 4. Try to create Loan Reschedule with new Interest Rate greater than zero to get the exception
@Test
public void uc157() {
final String operationDate = "1 April 2025";
AtomicLong createdLoanId = new AtomicLong();
runAt("1 April 2025", () -> {
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = create4IProgressive().interestRatePerPeriod(0.0).numberOfRepayments(4)//
.installmentAmountInMultiplesOf(null)//
.multiDisburseLoan(false)//
.disallowExpectedDisbursements(null)//
.allowApprovedDisbursedAmountsOverApplied(false)//
.overAppliedCalculationType(null)//
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
.overAppliedNumber(null)//
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(),
operationDate, 1000.0, 0.0, 4, null);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
createdLoanId.set(loanResponse.getLoanId());
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
.approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1000.0)));
});
runAt("1 May 2025", () -> {
executeInlineCOB(createdLoanId.get());
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
.transactionDate("1 May 2025").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(250.00));
});
runAt("6 May 2025", () -> {
executeInlineCOB(createdLoanId.get());
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
() -> loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
.loanId(createdLoanId.get()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("6 May 2025")
.newInterestRate(BigDecimal.valueOf(4.0)).rescheduleReasonId(1L).rescheduleFromDate("1 June 2025")));
LOG.info("ERROR: {}", callFailedRuntimeException.getMessage());
Assertions.assertTrue(
callFailedRuntimeException.getMessage().contains("Loan rescheduling is not allowed from interest rate 0 (zero)"));
});
}
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) { Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------"); LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");