FINERACT-2385: Zero amount reage transaction should not be allowed

This commit is contained in:
Oleksii Novikov
2025-10-27 10:45:14 +01:00
committed by Adam Saghy
parent 54e85c748d
commit 4ae6fd5694
6 changed files with 177 additions and 14 deletions
@@ -18,12 +18,15 @@
*/
package org.apache.fineract.test.stepdef.loan;
import static org.assertj.core.api.Assertions.assertThat;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.io.IOException;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import okhttp3.ResponseBody;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansResponse;
@@ -34,6 +37,7 @@ import org.apache.fineract.test.messaging.EventAssertion;
import org.apache.fineract.test.messaging.event.loan.LoanReAgeEvent;
import org.apache.fineract.test.stepdef.AbstractStepDef;
import org.apache.fineract.test.support.TestContextKey;
import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired;
import retrofit2.Response;
@@ -112,4 +116,34 @@ 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 {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.body().getLoanId();
final List<String> data = table.asLists().get(1);
final int frequencyNumber = Integer.parseInt(data.get(0));
final String frequencyType = data.get(1);
final String startDate = data.get(2);
final int numberOfInstallments = Integer.parseInt(data.get(3));
final PostLoansLoanIdTransactionsRequest reAgingRequest = LoanRequestFactory//
.defaultReAgingRequest()//
.frequencyNumber(frequencyNumber)//
.frequencyType(frequencyType)//
.startDate(startDate)//
.numberOfInstallments(numberOfInstallments);//
final Response<PostLoansLoanIdTransactionsResponse> response = loanTransactionsApi
.executeLoanTransaction(loanId, reAgingRequest, "reAge").execute();
try (ResponseBody errorBody = response.errorBody()) {
Assertions.assertNotNull(errorBody);
assertThat(errorBody.string()).contains(expectedError);
}
ErrorHelper.checkFailedApiCall(response, 403);
}
}
@@ -3063,3 +3063,106 @@ Feature: LoanReAging
| NSF fee | true | Specified due date | 01 October 2024 | Flat | 20.0 | 0.0 | 0.0 | 20.0 |
| NSF fee | true | Specified due date | 01 November 2024 | Flat | 30.0 | 0.0 | 0.0 | 30.0 |
Scenario: Verify that Loan re-aging with zero outstanding balance is rejected in real-time
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" 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_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 January 2024" 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 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
| 2 | 15 | 16 January 2024 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 3 | 15 | 31 January 2024 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 4 | 15 | 15 February 2024 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
When Admin sets the business date to "20 February 2024"
And Customer makes "AUTOPAY" repayment on "20 February 2024" with 750 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 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
| 2 | 15 | 16 January 2024 | 20 February 2024 | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 3 | 15 | 31 January 2024 | 20 February 2024 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 4 | 15 | 15 February 2024 | 20 February 2024 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
And Admin adds "LOAN_NSF_FEE" due date charge with "01 March 2024" due date and 10 EUR transaction amount
Then Loan Repayment schedule has 5 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 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
| 2 | 15 | 16 January 2024 | 20 February 2024 | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 3 | 15 | 31 January 2024 | 20 February 2024 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 4 | 15 | 15 February 2024 | 20 February 2024 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 5 | 15 | 01 March 2024 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
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:
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
| 1 | MONTHS | 01 March 2024 | 3 |
Then Loan Repayment schedule has 5 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 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
| 2 | 15 | 16 January 2024 | 20 February 2024 | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 3 | 15 | 31 January 2024 | 20 February 2024 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 4 | 15 | 15 February 2024 | 20 February 2024 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 5 | 15 | 01 March 2024 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 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 |
Then Loan Charges tab has the following data:
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
| NSF fee | true | Specified due date | 01 March 2024 | Flat | 10.0 | 0.0 | 0.0 | 10.0 |
Scenario: Verify that zero amount reage transaction is reverted during reverse-replay
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" 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_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 January 2024" 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 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
| 2 | 15 | 16 January 2024 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 3 | 15 | 31 January 2024 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 4 | 15 | 15 February 2024 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
When Admin sets the business date to "20 February 2024"
When Admin creates a Loan re-aging transaction with the following data:
| frequencyNumber | frequencyType | startDate | numberOfInstallments |
| 1 | MONTHS | 01 March 2024 | 3 |
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 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
| 2 | 15 | 16 January 2024 | 20 February 2024 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 | 15 | 31 January 2024 | 20 February 2024 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 4 | 15 | 15 February 2024 | 20 February 2024 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| 5 | 15 | 01 March 2024 | | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 6 | 31 | 01 April 2024 | | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
| 7 | 30 | 01 May 2024 | | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 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 | Re-age | 750.0 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | false |
When Admin sets the business date to "21 February 2024"
And Customer makes "AUTOPAY" repayment on "19 February 2024" with 750 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 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 0 | 01 January 2024 | 01 January 2024 | 750.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 |
| 2 | 15 | 16 January 2024 | 19 February 2024 | 500.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 3 | 15 | 31 January 2024 | 19 February 2024 | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
| 4 | 15 | 15 February 2024 | 19 February 2024 | 0.0 | 250.0 | 0.0 | 0.0 | 0.0 | 250.0 | 250.0 | 0.0 | 250.0 | 0.0 |
Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 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 |
| 19 February 2024 | Repayment | 750.0 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | false |
@@ -2902,6 +2902,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency),
Money.zero(currency));
if (outstandingPrincipalBalance.get().isZero()) {
loanTransaction.reverse();
return;
}
Money calculatedPrincipal = Money.zero(currency);
Money adjustCalculatedPrincipal = Money.zero(currency);
if (outstandingPrincipalBalance.get().isGreaterThanZero()) {
@@ -56,6 +56,7 @@ public class LoanReAgingValidator {
public void validateReAge(Loan loan, JsonCommand command) {
validateReAgeRequest(loan, command);
validateReAgeBusinessRules(loan);
validateReAgeOutstandingBalance(loan, command);
}
private void validateReAgeRequest(Loan loan, JsonCommand command) {
@@ -180,4 +181,20 @@ public class LoanReAgingValidator {
private boolean transactionHappenedAfterOther(LoanTransaction transaction, LoanTransaction otherTransaction) {
return new ChangeOperation(transaction).compareTo(new ChangeOperation(otherTransaction)) > 0;
}
private void validateReAgeOutstandingBalance(final Loan loan, final JsonCommand command) {
final LocalDate businessDate = getBusinessLocalDate();
final LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
final boolean isBackdated = businessDate.isAfter(startDate);
if (isBackdated) {
return;
}
if (loan.getSummary().getTotalPrincipalOutstanding().compareTo(java.math.BigDecimal.ZERO) == 0) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.no.outstanding.balance.to.reage",
"Loan cannot be re-aged as there are no outstanding balances to be re-aged", loan.getId());
}
}
}
@@ -44,6 +44,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
@@ -516,6 +517,9 @@ class LoanReAgingValidatorTest {
given(loanProductRelatedDetail.getLoanScheduleType()).willReturn(LoanScheduleType.PROGRESSIVE);
given(loan.isInterestBearing()).willReturn(false);
given(loan.getLoanTransactions()).willReturn(List.of());
LoanSummary loanSummary = mock(LoanSummary.class);
given(loan.getSummary()).willReturn(loanSummary);
given(loanSummary.getTotalPrincipalOutstanding()).willReturn(java.math.BigDecimal.valueOf(1000));
return loan;
}
@@ -537,15 +537,15 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
long loanId = createdLoanId.get();
PostLoansLoanIdTransactionsResponse repaymentResponse = loanTransactionHelper.makeLoanRepayment(loanId,
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("02 February 2023").locale("en")
.transactionAmount(250.0));
.transactionAmount(200.0));
// verify transactions
verifyTransactions(loanId, //
transaction(500.0, "Disbursement", "01 January 2023"), //
transaction(125.0, "Down Payment", "01 January 2023"), //
transaction(125.0, "Repayment", "01 February 2023"), //
transaction(250.0, "Repayment", "02 February 2023"), //
transaction(0.0, "Re-age", "27 February 2023") //
transaction(200.0, "Repayment", "02 February 2023"), //
transaction(50.0, "Re-age", "27 February 2023") //
);
verifyRepaymentSchedule(loanId, //
@@ -553,17 +553,17 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
installment(125.0, true, "01 January 2023"), //
installment(125.0, true, "16 January 2023"), //
installment(125.0, true, "31 January 2023"), //
installment(125.0, true, "15 February 2023"), //
installment(0.0, true, "01 March 2023"), //
installment(0.0, true, "01 April 2023"), //
installment(0.0, true, "01 May 2023"), //
installment(0.0, true, "01 June 2023"), //
installment(0.0, true, "01 July 2023"), //
installment(0.0, true, "01 August 2023") //
installment(75.00, true, "15 February 2023"), //
installment(8.33, false, "01 March 2023"), //
installment(8.33, false, "01 April 2023"), //
installment(8.33, false, "01 May 2023"), //
installment(8.33, false, "01 June 2023"), //
installment(8.33, false, "01 July 2023"), //
installment(8.35, false, "01 August 2023") //
);
checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 2, 2));
checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1));
verifyLoanStatus(loanId, LoanStatus.CLOSED_OBLIGATIONS_MET);
verifyLoanStatus(loanId, LoanStatus.ACTIVE);
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentResponse.getResourceId(),
new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("28 February 2023")
@@ -574,7 +574,7 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
transaction(500.0, "Disbursement", "01 January 2023"), //
transaction(125.0, "Down Payment", "01 January 2023"), //
transaction(125.0, "Repayment", "01 February 2023"), //
reversedTransaction(250.0, "Repayment", "02 February 2023"), //
reversedTransaction(200.0, "Repayment", "02 February 2023"), //
transaction(250.0, "Re-age", "27 February 2023") //
);
@@ -607,7 +607,7 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
transaction(500.0, "Disbursement", "01 January 2023"), //
transaction(125.0, "Down Payment", "01 January 2023"), //
transaction(125.0, "Repayment", "01 February 2023"), //
reversedTransaction(250.0, "Repayment", "02 February 2023"), //
reversedTransaction(200.0, "Repayment", "02 February 2023"), //
reversedTransaction(250.0, "Re-age", "27 February 2023") //
);