FINERACT-2389: Reprocess Interest Refund txn amount if no txn was changed

This commit is contained in:
Jose Alberto Hernandez
2026-01-13 10:05:19 +01:00
committed by Adam Saghy
parent 0026aaf1a6
commit 6df78f64c0
4 changed files with 76 additions and 24 deletions
@@ -568,12 +568,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.filter(LoanTransaction::isNotReversed).map(AbstractPersistableCustom::getId).toList();
final List<LoanTransaction> modifiedTransactions = new ArrayList<>(progCtx.getAlreadyProcessedTransactions().stream()
.filter(LoanTransaction::isNotReversed).filter(tr -> tr.getId() == null).toList());
if (!modifiedTransactions.isEmpty()) {
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate,
modifiedTransactions, unmodifiedTransactionIds);
final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
loanTransaction.updateAmount(newAmount.getAmount());
}
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate,
modifiedTransactions, unmodifiedTransactionIds);
final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
loanTransaction.updateAmount(newAmount.getAmount());
progCtx.setSumOfInterestRefundAmount(progCtx.getSumOfInterestRefundAmount().add(loanTransaction.getAmount()));
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.integrationtests;
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.DATE_FORMAT;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.REPLAYED;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -32,6 +33,7 @@ import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.AdvancedPaymentData;
@@ -41,6 +43,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionId
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
import org.apache.fineract.client.util.CallFailedRuntimeException;
@@ -1633,4 +1636,55 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
Assertions.assertEquals(0.36, Utils.getDoubleValue(loanDetails.getTotalOverpaid()));
});
}
@Test
public void allowToReprocessInterestRefundEvenIfNoTransactionWasChanged() {
runAt("1 February 2025", () -> {
Long loanProductId = getOrCreateLoanProduct();
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductId, "1 January 2025", 100.0, 26.0, 6, null);
Assertions.assertNotNull(loanId);
disburseLoan(loanId, BigDecimal.valueOf(100.0), "1 January 2025");
final String transactionExternalId = UUID.randomUUID().toString();
final PostLoansLoanIdTransactionsResponse refundResponse = loanTransactionHelper
.makeMerchantIssuedRefund(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN)
.transactionDate("1 February 2025").locale("en").transactionAmount(66.41).externalId(transactionExternalId).interestRefundCalculation(false));
Assertions.assertNotNull(refundResponse.getResourceId());
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2025"), //
transaction(66.41, "Merchant Issued Refund", "01 February 2025") //
);
// Create manual interest refund via API
PostLoansLoanIdTransactionsResponse interestRefundResponse = loanTransactionHelper.createManualInterestRefund(loanId, refundResponse.getResourceId(),
"1 February 2025", 1.47, null);
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2025"), //
transaction(66.41, "Merchant Issued Refund", "01 February 2025"), //
transaction(1.47, "Interest Refund", "01 February 2025") //
);
PostLoansLoanIdTransactionsResponse repaymentResponse = loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "20 January 2025", 17.94);
loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "25 January 2025", 10.94);
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2025"), //
transaction(17.94, "Repayment", "20 January 2025"), //
transaction(10.94, "Repayment", "25 January 2025"), //
transaction(66.41, "Merchant Issued Refund", "01 February 2025"), //
transaction(1.47, "Interest Refund", "01 February 2025") //
);
loanTransactionHelper.reverseLoanTransaction(loanId, repaymentResponse.getResourceId(),
new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN)
.transactionDate("25 January 2021").transactionAmount(0.0).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
verifyLoanStatus(loanDetails, LoanStatus.OVERPAID);
Assertions.assertEquals(0.36, Utils.getDoubleValue(loanDetails.getTotalOverpaid()));
});
}
}
@@ -96,7 +96,7 @@ public class LoanManualInterestRefundResponseStructureTest extends BaseLoanInteg
targetTransactionIdRef.set(refundResponse.getResourceId());
// Create manual interest refund via API
PostLoansLoanIdTransactionsResponse interestRefundResponse = createManualInterestRefund(loanId, refundResponse.getResourceId(),
PostLoansLoanIdTransactionsResponse interestRefundResponse = loanTransactionHelper.createManualInterestRefund(loanId, refundResponse.getResourceId(),
"15 January 2024", 5.0, null);
assertNotNull(interestRefundResponse, "Interest refund response should not be null");
@@ -157,7 +157,7 @@ public class LoanManualInterestRefundResponseStructureTest extends BaseLoanInteg
// Create manual interest refund with external ID
String interestRefundExternalId = UUID.randomUUID().toString();
PostLoansLoanIdTransactionsResponse interestRefundResponse = createManualInterestRefund(loanId, refundResponse.getResourceId(),
PostLoansLoanIdTransactionsResponse interestRefundResponse = loanTransactionHelper.createManualInterestRefund(loanId, refundResponse.getResourceId(),
"15 February 2024", 5.0, interestRefundExternalId);
assertNotNull(interestRefundResponse, "Interest refund response should not be null");
@@ -180,22 +180,6 @@ public class LoanManualInterestRefundResponseStructureTest extends BaseLoanInteg
});
}
/**
* Helper method to create manual interest refund transaction
*/
private PostLoansLoanIdTransactionsResponse createManualInterestRefund(Long loanId, Long targetTransactionId, String transactionDate,
Double amount, String externalId) {
PostLoansLoanIdTransactionsTransactionIdRequest request = new PostLoansLoanIdTransactionsTransactionIdRequest()
.transactionAmount(amount).dateFormat("dd MMMM yyyy").locale("en");
if (externalId != null) {
request.externalId(externalId);
}
return loanTransactionHelper.manualInterestRefund(loanId, targetTransactionId, request);
}
/**
* Helper method to make loan merchant issued refund (without automatic interest refund)
*/
@@ -1709,6 +1709,22 @@ public class LoanTransactionHelper {
excludedTransactionTypes, page, size, sort));
}
/**
* Helper method to create manual interest refund transaction
*/
public PostLoansLoanIdTransactionsResponse createManualInterestRefund(Long loanId, Long targetTransactionId, String transactionDate,
Double amount, String externalId) {
PostLoansLoanIdTransactionsTransactionIdRequest request = new PostLoansLoanIdTransactionsTransactionIdRequest()
.transactionAmount(amount).dateFormat("dd MMMM yyyy").locale("en");
if (externalId != null) {
request.externalId(externalId);
}
return manualInterestRefund(loanId, targetTransactionId, request);
}
// TODO: Rewrite to use fineract-client instead!
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)