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 extraTerms = (rescheduleData.get(5) == null || "0".equals(rescheduleData.get(5))) ? null
: Integer.valueOf(rescheduleData.get(5));
BigDecimal newInterestRate = (rescheduleData.get(6) == null || "0".equals(rescheduleData.get(6))) ? null
: new BigDecimal(rescheduleData.get(6));
BigDecimal newInterestRate = (rescheduleData.get(6) == null) ? null : new BigDecimal(rescheduleData.get(6));
PostCreateRescheduleLoansRequest request = new PostCreateRescheduleLoansRequest()//
.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 |
| | | 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 |
| 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 |
| 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 |
| 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.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:
| 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 |
@@ -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 |
| | | 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 |
| 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 |
| 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 |
| 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.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:
| 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 |
@@ -1788,4 +1788,4 @@ Feature: Credit Balance Refund
| 25 March 2025 | Credit Balance Refund | 122.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false |
| 01 April 2025 | Disbursement | 243.79 | 0.0 | 0.0 | 0.0 | 0.0 | 243.79 | false | false |
| 01 April 2025 | Down Payment | 61.0 | 61.0 | 0.0 | 0.0 | 0.0 | 182.79 | false | false |
Then Loan status will be "ACTIVE"
Then Loan status will be "ACTIVE"
@@ -917,141 +917,92 @@ Feature: LoanReschedule
And Loan Transactions tab has the following data:
| 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 |
@TestRailId:C3809
Scenario: Verify change interest rate with 0->4 reschedule scenario - UC1
When Admin sets the business date to "01 April 2025"
@TestRailId:C3882
Scenario: Reschedule progressive loan to 0 percent interest then back to 10 percent
When Admin sets the business date to "1 January 2024"
When Admin creates a client with random 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 |
| 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 |
And Admin successfully approves the loan on "01 April 2025" with "1000" amount and expected disbursement date on "01 April 2025"
When Admin successfully disburse the loan on "01 April 2025" with "1000" EUR transaction amount
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 | | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 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 |
| 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 |
| 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 | 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 "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024"
When Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount
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 |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.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 | 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 | 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 | 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:
| 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 |
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 |
When Admin sets the business date to "01 May 2025"
And Admin runs inline COB job for Loan
And Customer makes "AUTOPAY" repayment on "01 May 2025" with 250.0 EUR transaction amount
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 | 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 | 0.0 | 0.0 | 0.0 | 250.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 |
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.93 | 0.0 | 0.0 | 102.93 | 0.0 | 0.0 | 0.0 | 102.93 |
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 02 January 2024 | 01 January 2024 | | | | | 0 |
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 |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 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 | 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 | 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 | 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:
| 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 |
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 |
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 to zero following data validation error should occur
Then Loan reschedule with the following data results a 400 error and "LOAN_RESCHEDULE_NOT_ALLOWED_FROM_ZERO_TO_NEW_INTEREST_RATE" error message
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 06 May 2025 | 05 May 2025 | | | | | 4 |
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-2665
# 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 | 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"
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 03 January 2024 | 01 January 2024 | | | | | 10 |
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 |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.91 | 0.0 | 0.0 | 102.91 | 0.0 | 0.0 | 0.0 | 102.91 |
@TestRailId:C3810
Scenario: Verify change interest rate with 4->0 reschedule scenario - UC2
When Admin sets the business date to "01 April 2025"
@TestRailId:C3883
Scenario: Create progressive loan with 0 percent interest then to 10 percent after first period
When Admin sets the business date to "1 January 2024"
When Admin creates a client with random 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 |
| 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 |
And Admin successfully approves the loan on "01 April 2025" with "1000" amount and expected disbursement date on "01 April 2025"
When Admin successfully disburse the loan on "01 April 2025" with "1000" EUR transaction amount
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 | | 751.24 | 248.76 | 3.33 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 |
| 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 |
| 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 |
| 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 |
| 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 | 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 "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024"
When Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount
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 |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 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 | 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 | 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 | 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:
| 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 |
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 |
When Admin sets the business date to "01 May 2025"
And Admin runs inline COB job for Loan
And Customer makes "AUTOPAY" repayment on "01 May 2025" with 252.09 EUR transaction amount
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.65 | 249.59 | 2.5 | 0.0 | 0.0 | 252.09 | 0.0 | 0.0 | 0.0 | 252.09 |
| 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 |
| 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 |
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
When Admin creates and approves Loan reschedule with the following data:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | graceOnPrincipal | graceOnInterest | extraTerms | newInterestRate |
| 02 February 2024 | 01 January 2024 | | | | | 10 |
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 |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 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 | 29 | 01 March 2024 | | 66.94 | 16.39 | 0.69 | 0.0 | 0.0 | 17.08 | 0.0 | 0.0 | 0.0 | 17.08 |
| 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 |
| 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 |
| 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 |
| 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 |
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.34 | 0.0 | 0.0 | 1008.34 | 252.09 | 0.0 | 0.0 | 756.25 |
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"
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.09 | 0.0 | 0.0 | 102.09 | 0.0 | 0.0 | 0.0 | 102.09 |
@@ -507,7 +507,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder();
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
&& loanTransaction.getLoan().isInterestRecalculationEnabled()) {
var model = progressiveTransactionCtx.getModel();
// Copy and paste Logic from super.handleCreditBalanceRefund
@@ -778,7 +778,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
if (loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
if (loanTransaction.getLoan().isInterestRecalculationEnabled()) {
processCreditTransactionWithEmiCalculator(loanTransaction, (ProgressiveTransactionCtx) ctx);
} else if (hasNoCustomCreditAllocationRule(loanTransaction)) {
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) {
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging
final Loan loan = disbursementTransaction.getLoan();
if (loan.isInterestBearing()) {
if (shouldUseEmiCalculation(transactionCtx, disbursementTransaction.getTransactionDate())) {
handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx);
} else {
handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx);
@@ -1356,7 +1355,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
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);
} else {
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) {
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.validateApprovalDate;
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.validateRescheduleReasonComment;
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonId;
@@ -78,8 +77,7 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
BigDecimal interestRate = validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper,
jsonElement, dataValidatorBuilder);
BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateUnsupportedParams(jsonElement, dataValidatorBuilder);
boolean hasInterestRateChange = interestRate != null;
@@ -197,4 +195,13 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
"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) {
if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
if (loan.isInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
reprocessLoanTransactionsService.reprocessTransactions(loan);
} else {
reprocessLoanTransactionsService.processLatestTransaction(transaction, loan);
@@ -161,6 +161,10 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
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.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> {
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.
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);
Money total = Money.of(currency, first.calculateTotalDisbursedAmountTillGivenPeriod(first.getLastInterestPeriod()))
Money total = Money
.of(currency, first.calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(first.getLastInterestPeriod()))
.plus(sumOfInterest).minus(alreadyRepaidPrincipals);
Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc);
@@ -150,7 +150,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
return BigDecimal.ZERO;
}
BigDecimal baseAmount = switch (method) {
case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAmountTillGivenPeriod(this));
case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(this));
case DECLINING_BALANCE -> getOutstandingLoanBalance().getAmount();
default -> throw new UnsupportedOperationException("Method not implemented: " + method);
};
@@ -84,6 +84,10 @@ public final class RepaymentPeriod {
@Getter
private BigDecimal totalDisbursedAmount;
@Setter
@Getter
private BigDecimal totalCapitalizedIncomeAmount;
@JsonExclude
@Getter
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
@@ -389,16 +393,21 @@ public final class RepaymentPeriod {
/**
* @param tillPeriod
* 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) {
BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount());
public BigDecimal calculateTotalDisbursedAndCapitalizedIncomeAmountTillGivenPeriod(InterestPeriod tillPeriod) {
BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount()).add(MathUtil.nullToZero(getTotalCapitalizedIncomeAmount()));
for (InterestPeriod interestPeriod : this.interestPeriods) {
if (interestPeriod.equals(tillPeriod)) {
break;
}
if (!interestPeriod.getDueDate().equals(getFromDate()) && interestPeriod.getDisbursementAmount() != null) {
res = res.add(interestPeriod.getDisbursementAmount().getAmount());
if (!interestPeriod.getDueDate().equals(getFromDate())) {
if (interestPeriod.getDisbursementAmount() != null) {
res = res.add(interestPeriod.getDisbursementAmount().getAmount());
}
if (interestPeriod.getCapitalizedIncomePrincipal() != null) {
res = res.add(interestPeriod.getCapitalizedIncomePrincipal().getAmount());
}
}
}
return res;
@@ -535,7 +535,6 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Loan loan = mock(Loan.class);
when(loan.getLoanRepaymentScheduleDetail()).thenReturn(loanProductRelatedDetail);
when(loan.isInterestBearing()).thenReturn(true);
LoanRepaymentScheduleInstallment installment1 = spy(
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) {
Optional<ProgressiveLoanInterestScheduleModel> savedModel = modelRepository.getSavedModel(loan,
loanTransaction.getTransactionDate());
ProgressiveLoanInterestScheduleModel model = savedModel
.orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate()));
ProgressiveTransactionCtx progressiveContext = new ProgressiveTransactionCtx(loan.getCurrency(),
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.setChargedOff(loan.isChargedOff());
progressiveContext.setContractTerminated(loan.isContractTermination());
ChangedTransactionDetail result = advancedProcessor.processLatestTransaction(loanTransaction, progressiveContext);
if (savedModel.isPresent() && !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
modelRepository.writeInterestScheduleModel(loan, savedModel.get());
if (!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
modelRepository.writeInterestScheduleModel(loan, model);
}
return result;
}
@@ -124,7 +126,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(
transactionProcessingStrategyCode);
if (loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
&& loanTransaction.getLoan().isInterestRecalculationEnabled()) {
return processLatestTransactionProgressiveInterestRecalculation(advancedProcessor, loanTransaction.getLoan(), loanTransaction);
}
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.LoanTransaction;
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.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.springframework.stereotype.Service;
@@ -145,10 +148,25 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
@Override
public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) {
final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService.processLatestTransaction(
loan.getTransactionProcessingStrategyCode(), loanTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail()));
LoanRepaymentScheduleTransactionProcessor transactionProcessor = loanTransactionProcessingService
.getTransactionProcessor(loan.getTransactionProcessingStrategyCode());
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()
.map(TransactionChangeData::getNewTransaction).peek(transaction -> transaction.updateLoan(loan)).toList();
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,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");