FINERACT-2354: Third step - backdated re-age edge case for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change

This commit is contained in:
mariiaKraievska
2025-11-13 08:44:22 +01:00
committed by Adam Saghy
parent 01ca515e8f
commit 5a1c6edbef
7 changed files with 438 additions and 141 deletions
@@ -233,8 +233,9 @@ public class LoanReAgingStepDef extends AbstractStepDef {
eventAssertion.assertEventRaised(LoanReAgeEvent.class, loanId);
}
@When("Admin fails to create a Loan re-aging transaction with error {string} and with the following data:")
public void adminFailsToCreateReAgingTransactionWithError(final String expectedError, final DataTable table) throws IOException {
@When("Admin fails to create a Loan re-aging transaction with status code {int} error {string} and with the following data:")
public void adminFailsToCreateReAgingTransactionWithError(final int statusCode, final String expectedError, final DataTable table)
throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.body().getLoanId();
@@ -259,7 +260,7 @@ public class LoanReAgingStepDef extends AbstractStepDef {
assertThat(errorBody.string()).contains(expectedError);
}
ErrorHelper.checkFailedApiCall(response, 403);
ErrorHelper.checkFailedApiCall(response, statusCode);
}
@Then("Admin fails to create a Loan re-aging transaction with the following data because loan was charged-off:")
@@ -3473,7 +3473,7 @@ Feature: LoanReAging
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 01 January 2024 | Down Payment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 750.0 | false |
| 20 February 2024 | Repayment | 750.0 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | false |
When Admin fails to create a Loan re-aging transaction with error "error.msg.loan.reage.no.outstanding.balance.to.reage" and with the following data:
When Admin fails to create a Loan re-aging transaction with status code 403 error "error.msg.loan.reage.no.outstanding.balance.to.reage" and with the following data:
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
| 1 | MONTHS | 01 March 2024 | 3 |
Then Loan Repayment schedule has 5 periods, with the following data for periods:
@@ -5354,6 +5354,242 @@ Feature: LoanReAging
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 15 March 2024 | Re-age | 84.28 | 83.57 | 0.71 | 0.0 | 0.0 | 0.0 | true | true |
| 16 March 2024 | Repayment | 14.3 | 13.81 | 0.49 | 0.0 | 0.0 | 69.76 | false | true |
When Loan Pay-off is made on "01 April 2024"
Then Loan is closed with zero outstanding balance and it's all installments have obligations met
Then Loan is closed with zero outstanding balance and it's all installments have obligations met
@TestRailId:C4110 @AdvancedPaymentAllocation
Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - backdated re-aging transaction - UC16
When Admin sets the business date to "01 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_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
When Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false |
When Admin sets the business date to "01 March 2024"
And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
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.05 | 0.0 | 0.0 | 102.05 | 34.02 | 0.0 | 0.0 | 68.03 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false |
When Admin sets the business date to "01 June 2024"
# Backdated re-aging - created in June but effective from April 01
When Admin creates a Loan re-aging transaction with the following data:
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
| 1 | MONTHS | 01 April 2024 | 6 |
Then Loan Repayment schedule has 8 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 56.0 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 |
| 4 | 30 | 01 May 2024 | | 44.95 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 |
| 5 | 31 | 01 June 2024 | | 33.9 | 11.05 | 0.39 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 |
| 6 | 30 | 01 July 2024 | | 22.66 | 11.24 | 0.2 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 |
| 7 | 31 | 01 August 2024 | | 11.35 | 11.31 | 0.13 | 0.0 | 0.0 | 11.44 | 0.0 | 0.0 | 0.0 | 11.44 |
| 8 | 31 | 01 September 2024| | 0.0 | 11.35 | 0.07 | 0.0 | 0.0 | 11.42 | 0.0 | 0.0 | 0.0 | 11.42 |
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.64 | 0.0 | 0.0 | 102.64 | 34.02 | 0.0 | 0.0 | 68.62 |
Then 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 |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 April 2024 | Re-age | 67.44 | 67.05 | 0.39 | 0.0 | 0.0 | 0.0 | false | true |
@TestRailId:C4154 @AdvancedPaymentAllocation
Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - re-aging on same day as disbursement - UC16.1
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
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_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
When Admin creates a Loan re-aging transaction with the following data:
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
| 1 | MONTHS | 01 January 2024 | 6 |
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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
| 01 January 2024 | Re-age | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | false |
@TestRailId:C4154 @AdvancedPaymentAllocation
Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - validation that reAgeStartDate must be after disbursement date - UC16.2
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin set "LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
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_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
When Admin sets the business date to "15 February 2024"
When Admin fails to create a Loan re-aging transaction with status code 400 error "validation.msg.validation.errors.exist" and with the following data:
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
| 1 | MONTHS | 31 December 2023 | 6 |
@TestRailId:C4156 @AdvancedPaymentAllocation
Scenario: Verify allowing Re-aging on interest bearing loan - Interest calculation: Default Behavior - Verify backdated re-aging on the day of repayment with interest recalculation enabled - UC16.3
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
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_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 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.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
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.05 | 0.0 | 0.0 | 102.05 | 0.0 | 0.0 | 0.0 | 102.05 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
When Admin sets the business date to "01 February 2024"
And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
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.05 | 0.0 | 0.0 | 102.05 | 17.01 | 0.0 | 0.0 | 85.04 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false |
When Admin sets the business date to "01 March 2024"
And Customer makes "AUTOPAY" repayment on "01 March 2024" with 17.01 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 |
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.05 | 0.0 | 0.0 | 102.05 | 34.02 | 0.0 | 0.0 | 68.03 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false |
When Admin sets the business date to "01 June 2024"
When Admin creates a Loan re-aging transaction with the following data:
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
| 1 | MONTHS | 01 February 2024 | 6 |
#Internal Server Error 500 is seen here, needs fixing
Then Loan Repayment schedule has 7 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 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 2 | 29 | 01 March 2024 | 01 March 2024 | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2024 | | 56.02 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 |
| 4 | 30 | 01 May 2024 | | 44.99 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 |
| 5 | 31 | 01 June 2024 | | 33.96 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 |
| 6 | 30 | 01 July 2024 | | 22.93 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 | 0.0 | 0.0 | 0.0 | 11.03 |
| 7 | 31 | 01 August 2024 | | 0.0 | 22.93 | 0.0 | 0.0 | 0.0 | 22.93 | 0.0 | 0.0 | 0.0 | 22.93 |
Then Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 1.07 | 0.0 | 0.0 | 101.07 | 34.02 | 0.0 | 0.0 | 67.05 |
Then 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 |
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false | false |
| 01 March 2024 | Repayment | 17.01 | 16.52 | 0.49 | 0.0 | 0.0 | 67.05 | false | false |
| 01 March 2024 | Re-age | 67.05 | 67.05 | 0.0 | 0.0 | 0.0 | 0.0 | false | true |
@@ -1578,49 +1578,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
}
private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(LocalDate targetDate, ProgressiveLoanInterestScheduleModel model) {
return model.repaymentPeriods().stream() //
.filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).toList();
}
public boolean rework(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
boolean hasChange = false;
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model);
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod();
RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod);
MonetaryCurrency currency = model.zero().getCurrency();
Money overDuePrincipal = Money.zero(currency);
Money aggregatedOverDuePrincipal = Money.zero(currency);
for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) {
// add and subtract outstanding principal
if (!overDuePrincipal.isZero()) {
boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
hasChange = hasChange || currentChanges;
}
overDuePrincipal = processingPeriod.getOutstandingPrincipal();
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
}
if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) {
boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal, aggregatedOverDuePrincipal,
ctx);
hasChange = hasChange || currentChanges;
}
if (aggregatedOverDuePrincipal.isGreaterThanZero()
&& (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) {
model.lastOverdueBalanceChange(targetDate);
}
}
return hasChange;
}
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
recalculateInterestForDate(targetDate, ctx, true);
}
@@ -1631,7 +1588,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (isInterestRecalculationSupported(ctx, loan) && !loan.isNpa()
&& !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
boolean modelHasUpdates = rework(targetDate, ctx);
boolean modelHasUpdates = emiCalculator.recalculateModelOverdueAmountsTillDate(ctx, targetDate);
if (modelHasUpdates && updateInstallments) {
updateInstallmentsPrincipalAndInterestByModel(ctx);
}
@@ -1639,38 +1596,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private boolean adjustOverduePrincipal(LocalDate currentDate, RepaymentPeriod currentInstallment, Money overduePrincipal,
Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
LocalDate fromDate = currentInstallment.getFromDate();
LocalDate toDate = currentInstallment.getDueDate();
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
boolean prepayAttempt = ctx.isPrepayAttempt();
LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan()
.getLoanInterestRecalculationDetails();
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
emiCalculator.addBalanceCorrection(model, fromDate, overduePrincipal);
} else {
emiCalculator.addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
}
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
LocalDate lastOverdueBalanceChange;
if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) {
lastOverdueBalanceChange = toDate;
} else {
lastOverdueBalanceChange = currentDate;
}
emiCalculator.addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
}
return true;
}
return false;
}
private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) {
ctx.getModel().repaymentPeriods().forEach(repayment -> {
LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream()
@@ -2991,9 +2916,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
private void cleanupInstallmentsAndRepaymentPeriodsAfterReAging(final ProgressiveTransactionCtx ctx) {
private void cleanupInstallmentsAfterReAging(final ProgressiveTransactionCtx ctx) {
final List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
final List<RepaymentPeriod> repaymentPeriods = ctx.getModel().repaymentPeriods();
// Find the last re-aged installment number
final OptionalInt lastReAgedInstallmentNumberOpt = installments.stream().filter(LoanRepaymentScheduleInstallment::isReAged)
@@ -3001,17 +2925,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (lastReAgedInstallmentNumberOpt.isPresent()) {
final int lastReAgedInstallmentNumber = lastReAgedInstallmentNumberOpt.getAsInt();
final LoanRepaymentScheduleInstallment lastReAgedInstallment = installments.stream()
.filter(i -> i.getInstallmentNumber().equals(lastReAgedInstallmentNumber)).findFirst().orElse(null);
// Remove installments with numbers greater than the last re-aged installment
final List<LoanRepaymentScheduleInstallment> installmentsToRemove = installments.stream().filter(i -> i != null
&& !i.isAdditional() && i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber)
.toList();
if (lastReAgedInstallment != null) {
final List<RepaymentPeriod> repaymentPeriodsToRemove = repaymentPeriods.stream()
.filter(rp -> !rp.getFromDate().isBefore(lastReAgedInstallment.getDueDate())).toList();
repaymentPeriodsToRemove.forEach(repaymentPeriods::remove);
}
installmentsToRemove.forEach(installments::remove);
}
}
@@ -3050,7 +2967,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
});
cleanupInstallmentsAndRepaymentPeriodsAfterReAging(ctx);
cleanupInstallmentsAfterReAging(ctx);
}
private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) {
@@ -3307,47 +3224,29 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
final MonetaryCurrency currency = ctx.getCurrency();
final Loan loan = loanTransaction.getLoan();
final MathContext mc = MoneyHelper.getMathContext();
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
final List<RepaymentPeriod> repaymentPeriods = ctx.getModel().repaymentPeriods();
final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate();
final List<RepaymentPeriod> periodsBeforeReAging = repaymentPeriods.stream()
.filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList();
final List<LoanRepaymentScheduleInstallment> installmentsBeforeReAging = installments.stream()
.filter(rp -> rp.getFromDate().isBefore(reAgingStartDate)).toList();
final RepaymentPeriod lastPeriod = periodsBeforeReAging.getLast();
// Define the date, which must match reAgingStartDate
final LocalDate expectedReAgingDate = installmentsBeforeReAging.isEmpty() ? installments.getFirst().getFromDate()
: installmentsBeforeReAging.getLast().getDueDate();
if (!lastPeriod.getDueDate().isEqual(reAgingStartDate)) {
if (!expectedReAgingDate.isEqual(reAgingStartDate)) {
// TODO: implement logic when re-aging changes the due dates
throw new NotImplementedException("Logic when re-aging changes the due dates not implemented");
}
final BigDecimal interestBeforeReAging = emiCalculator
.getPeriodInterestTillDate(ctx.getModel(), lastPeriod.getDueDate(), transactionDate, false).getAmount();
final AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency));
installments.forEach(i -> {
final Money principalOutstanding = i.getPrincipalOutstanding(currency);
if (principalOutstanding.isGreaterThanZero()) {
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
}
});
final AtomicReference<BigDecimal> interestFromZeroedInstallments = new AtomicReference<>(interestBeforeReAging);
installments.stream()
.filter(installment -> !installment.isObligationsMet() && !installment.getDueDate().isAfter(lastPeriod.getFromDate()))
.forEach(installment -> {
final BigDecimal currentInterest = interestFromZeroedInstallments.get();
final BigDecimal additionalInterest = MathUtil.nullToZero(installment.getInterestOutstanding(currency).getAmount()
.add(MathUtil.nullToZero(installment.getCreditedInterest()).negate()));
interestFromZeroedInstallments.set(currentInterest.add(additionalInterest));
});
final Money interestFromZeroedInstallments = emiCalculator.getOutstandingInterestTillDate(ctx.getModel(),
loanTransaction.getTransactionDate());
final BigDecimal interestRate = loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate();
final Money totalOutstandingPrincipal = ctx.getModel().getTotalOutstandingPrincipal();
final LoanApplicationTerms loanApplicationTerms = new LoanApplicationTerms.Builder().currency(currency.getCurrencyData())
.repaymentsStartingFromDate(reAgingStartDate).principal(outstandingPrincipalBalance.get())
.repaymentsStartingFromDate(reAgingStartDate).principal(totalOutstandingPrincipal)
.loanTermFrequency(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())
.loanTermPeriodFrequencyType(loanTransaction.getLoanReAgeParameter().getFrequencyType())
.numberOfRepayments(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())
@@ -3366,11 +3265,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.mc(mc).build();
// Update the existing model with re-aged periods
emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx.getModel(), loanTransaction, loanApplicationTerms, mc);
emiCalculator.updateModelRepaymentPeriodsDuringReAge(ctx, loanTransaction, loanApplicationTerms, mc);
updateInstallmentsByModelForReAging(loanTransaction, ctx);
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(),
Money.of(currency, interestFromZeroedInstallments.get()), Money.zero(currency), Money.zero(currency));
loanTransaction.updateComponentsAndTotal(totalOutstandingPrincipal, interestFromZeroedInstallments, Money.zero(currency),
Money.zero(currency));
reprocessInstallments(installments);
}
}
@@ -28,6 +28,7 @@ import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
@@ -143,6 +144,14 @@ public interface EMICalculator {
*/
void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate);
void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel scheduleModel, LoanTransaction loanTransaction,
void updateModelRepaymentPeriodsDuringReAge(ProgressiveTransactionCtx ctx, LoanTransaction loanTransaction,
LoanApplicationTerms loanApplicationTerms, MathContext mc);
boolean recalculateModelOverdueAmountsTillDate(ProgressiveTransactionCtx ctx, LocalDate targetDate);
/**
* Gives back the sum of the outstanding interest from the whole model till the provided date.
*/
@NotNull
Money getOutstandingInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate tillDate);
}
@@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
@@ -46,10 +47,13 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
@@ -571,20 +575,126 @@ public final class ProgressiveEMICalculator implements EMICalculator {
}
@Override
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LoanTransaction loanTransaction, final LoanApplicationTerms loanApplicationTerms, final MathContext mc) {
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveTransactionCtx ctx, final LoanTransaction loanTransaction,
final LoanApplicationTerms loanApplicationTerms, final MathContext mc) {
final List<RepaymentPeriod> existingRepaymentPeriods = ctx.getModel().repaymentPeriods();
moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, reAgingStartDate);
moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, loanTransaction.getLoanReAgeParameter().getStartDate());
final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanReAgeParameter);
final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanTransaction);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel(loanApplicationTerms,
mc, periodStartDate);
mergeNewInterestScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, loanTransaction);
mergeNewInterestScheduleModelWithExistingOne(ctx, temporaryReAgedScheduleModel, loanTransaction);
}
@Override
public boolean recalculateModelOverdueAmountsTillDate(final ProgressiveTransactionCtx ctx, final LocalDate targetDate) {
boolean hasChange = false;
final ProgressiveLoanInterestScheduleModel model = ctx.getModel();
final List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model);
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
final RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod();
final RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod);
final MonetaryCurrency currency = model.zero().getCurrency();
Money overDuePrincipal = Money.zero(currency);
Money aggregatedOverDuePrincipal = Money.zero(currency);
for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) {
// add and subtract outstanding principal
if (!overDuePrincipal.isZero()) {
final boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
hasChange = hasChange || currentChanges;
}
overDuePrincipal = processingPeriod.getOutstandingPrincipal();
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
}
if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) {
final boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
hasChange = hasChange || currentChanges;
}
if (aggregatedOverDuePrincipal.isGreaterThanZero()
&& (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) {
model.lastOverdueBalanceChange(targetDate);
}
}
return hasChange;
}
@Override
public Money getOutstandingInterestTillDate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate tillDate) {
final ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
tillDate);
return recalculatedScheduleModelTillDate.repaymentPeriods().stream()
.filter(repaymentPeriod -> repaymentPeriod.getFromDate().isBefore(tillDate)).map(RepaymentPeriod::getOutstandingInterest)
.reduce(scheduleModel.zero(), Money::add);
}
private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(final LocalDate targetDate,
final ProgressiveLoanInterestScheduleModel model) {
return model.repaymentPeriods().stream() //
.filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).toList();
}
private boolean adjustOverduePrincipal(final LocalDate currentDate, final RepaymentPeriod currentInstallment,
final Money overduePrincipal, final Money aggregatedOverDuePrincipal, final ProgressiveTransactionCtx ctx) {
final LocalDate fromDate = currentInstallment.getFromDate();
final LocalDate toDate = currentInstallment.getDueDate();
final ProgressiveLoanInterestScheduleModel model = ctx.getModel();
final boolean prepayAttempt = ctx.isPrepayAttempt();
final LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan()
.getLoanInterestRecalculationDetails();
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
addBalanceCorrection(model, fromDate, overduePrincipal);
} else {
addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
}
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
LocalDate lastOverdueBalanceChange;
if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) {
lastOverdueBalanceChange = toDate;
} else {
lastOverdueBalanceChange = currentDate;
}
addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
}
return true;
}
return false;
}
private boolean shouldRecalculateTillInstallmentDueDate(final LoanInterestRecalculationDetails recalculationDetails,
final boolean isPrepayAttempt) {
// Rest frequency type and pre close interest calculation strategy can be controversial
// if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_PRE_CLOSURE_DATE
// no problem. Calculate till transaction date
// if restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy ==
// TILL_REST_FREQUENCY_DATE
// again, no problem. Calculate till due date of current installment
// if restFrequencyType == DAILY and preCloseInterestCalculationStrategy == TILL_REST_FREQUENCY_DATE
// or restFrequencyType == SAME_AS_REPAYMENT_PERIOD and preCloseInterestCalculationStrategy ==
// TILL_PRE_CLOSURE_DATE
// we cannot harmonize the two configs. Behaviour should mimic prepay api.
return switch (recalculationDetails.getRestFrequencyType()) {
case DAILY ->
isPrepayAttempt && recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
case SAME_AS_REPAYMENT_PERIOD ->
recalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillRestFrequencyEnabled();
case WEEKLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WEEKLY");
case MONTHLY -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: MONTHLY");
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
};
}
/**
@@ -592,18 +702,23 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* the balances of the updated model and also recalculate the EMI if the EMI of the last repayment period differs
* significantly from other periods.
*/
private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveLoanInterestScheduleModel scheduleModel,
private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveTransactionCtx ctx,
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LoanTransaction loanTransaction) {
final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods();
final ProgressiveLoanInterestScheduleModel scheduleModel = ctx.getModel();
final Loan loan = loanTransaction.getLoan();
if (newPeriods.isEmpty()) {
return;
}
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
final RepaymentPeriod firstRepaymentPeriod = existingRepaymentPeriods.getFirst();
final Money disbursedAmount = firstRepaymentPeriod.getTotalDisbursedAmount();
final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate();
final Optional<RepaymentPeriod> firstExistingRepaymentPeriodOpt = existingRepaymentPeriods.stream()
final Optional<RepaymentPeriod> firstExistingReAgedRepaymentPeriodOpt = existingRepaymentPeriods.stream()
.filter(period -> period.getDueDate().equals(reAgingStartDate)).findFirst();
for (final RepaymentPeriod newPeriod : newPeriods) {
@@ -611,8 +726,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
period -> period.getFromDate().equals(newPeriod.getFromDate()) && period.getDueDate().equals(newPeriod.getDueDate()))
.findFirst();
Optional<RepaymentPeriod> previousExistingRepaymentPeriodOpt = Optional.empty();
if (existingRepaymentPeriodOpt.isPresent() && firstExistingRepaymentPeriodOpt.isPresent()
&& existingRepaymentPeriodOpt.get().equals(firstExistingRepaymentPeriodOpt.get())) {
if (existingRepaymentPeriodOpt.isPresent() && firstExistingReAgedRepaymentPeriodOpt.isPresent()
&& existingRepaymentPeriodOpt.get().equals(firstExistingReAgedRepaymentPeriodOpt.get())) {
previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt.get().getPrevious();
}
@@ -623,25 +738,53 @@ public final class ProgressiveEMICalculator implements EMICalculator {
previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(),
newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(),
loanTransaction.getLoan().getLoanProductRelatedDetail());
rp.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount());
rp.setTotalDisbursedAmount(disbursedAmount);
if (existingRepaymentPeriodOpt.isPresent()) {
// Add an interest period on the disbursement date for the very first replaced period
if (existingRepaymentPeriodOpt.get().equals(firstRepaymentPeriod)) {
final LocalDate disbursementDate = loanTransaction.getLoan().getDisbursementDate();
rp.getInterestPeriods().getFirst().setDisbursementAmount(disbursedAmount);
rp.getInterestPeriods().getFirst().setFromDate(disbursementDate);
rp.getInterestPeriods().getFirst().setDueDate(disbursementDate);
rp.getInterestPeriods().add(InterestPeriod.withEmptyAmounts(rp, rp.getFromDate(), rp.getDueDate()));
}
existingRepaymentPeriods.remove(existingRepaymentPeriodOpt.get());
}
existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove);
existingRepaymentPeriods.add(rp);
calculateRateFactorForRepaymentPeriod(rp, scheduleModel);
}
final RepaymentPeriod lastReAgedInstallment = newPeriods.getLast();
if (reAgingStartDate.isBefore(DateUtils.getBusinessLocalDate()) && isInterestRecalculationIsAllowed(ctx, loan)) {
scheduleModel.lastOverdueBalanceChange(null);
recalculateModelOverdueAmountsTillDate(ctx, loanTransaction.getSubmittedOnDate());
}
final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream()
.filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate)
|| repaymentPeriod.getDueDate().isEqual(reAgingStartDate))
&& !repaymentPeriod.getDueDate().isAfter(lastReAgedInstallment.getDueDate()))
&& !repaymentPeriod.getDueDate().isAfter(newPeriods.getLast().getDueDate()))
.toList();
// cleanup repayment periods after re-aging
if (reAgedRepaymentPeriods.getLast() != null) {
final List<RepaymentPeriod> repaymentPeriodsToRemove = existingRepaymentPeriods.stream()
.filter(rp -> !rp.getFromDate().isBefore(reAgedRepaymentPeriods.getLast().getDueDate())).toList();
repaymentPeriodsToRemove.forEach(existingRepaymentPeriods::remove);
}
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, loanTransaction.getTransactionDate());
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods);
}
private boolean isInterestRecalculationIsAllowed(final ProgressiveTransactionCtx ctx, final Loan loan) {
return loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff() && !ctx.isWrittenOff()
&& !ctx.isContractTerminated() && !loan.isNpa()
&& !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue();
}
/**
* * Generates temporary interestScheduleModel with re-aged repayment periods
*/
@@ -663,8 +806,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* is used to generate re-aged repayment periods
*/
@NotNull
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanReAgeParameter loanReAgeParameter) {
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanTransaction loanTransaction) {
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
if (reAgingStartDate.isEqual(loanTransaction.getLoan().getDisbursementDate())) {
return reAgingStartDate;
}
return switch (loanReAgeParameter.getFrequencyType()) {
case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber());
case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber());
@@ -60,6 +60,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
private Money creditedPrincipal;
/** Stores credited interest. Related transaction: Chargeback */
private Money creditedInterest;
@Setter
private Money disbursementAmount;
private Money balanceCorrectionAmount;
private Money outstandingLoanBalance;
@@ -76,6 +76,7 @@ public class LoanReAgingValidator {
LocalDate startDate = command.localDateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
if (loan.isProgressiveSchedule()) {
// validate re-age transaction occurs after or on the disbursement date
baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate).notNull()
.validateDateAfterOrEqual(loan.getDisbursementDate());
} else {