This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit fb96df64ab44a9b0add1a703128be228cee6d4f5 Author: Jose Alberto Hernandez <[email protected]> AuthorDate: Wed Jan 21 19:49:28 2026 -0500 FINERACT-2421: Manual interest refund on closed loan generates exception --- .../LoanWritePlatformServiceJpaRepositoryImpl.java | 1 + .../integrationtests/LoanInterestRefundTest.java | 111 +++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 553a1bb640..0cfbcc3590 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -3016,6 +3016,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loanAccrualsProcessingService.reprocessExistingAccruals(loan, true); loanAccrualsProcessingService.processIncomePostingAndAccruals(loan, true); } + loanLifecycleStateMachine.determineAndTransition(loan, interestRefundTxn.getTransactionDate()); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java index e5e064192b..047c7eed76 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java @@ -34,6 +34,7 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.GetLoansLoanIdResponse; @@ -1512,6 +1513,116 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest { }); } + // UC20: Manual interest refund on closed loan results in DatabaseException + @Test + public void verifyUC20() { + AtomicReference<Long> loanIdRef = new AtomicReference<>(); + final Integer totalTransactions = 4; + AtomicReferenceArray<PostLoansLoanIdTransactionsResponse> merchantIssuedRefundTransactions = new AtomicReferenceArray<>( + totalTransactions); + runAt("07 March 2025", () -> { + PostLoanProductsResponse loanProduct = loanProductHelper + .createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .chargeOffBehaviour("ZERO_INTEREST")// + .enableAccrualActivityPosting(true)// + .allowApprovedDisbursedAmountsOverApplied(true)// + .overAppliedCalculationType("flat")// + .overAppliedNumber(10000)// + .enableInstallmentLevelDelinquency(true)// + .multiDisburseLoan(true)// + .loanScheduleType("PROGRESSIVE")// + .loanScheduleProcessingType("HORIZONTAL")// + .interestRecognitionOnDisbursementDate(true)// + .disallowExpectedDisbursements(true)// + .maxTrancheCount(500)// + .addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) // + .addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.PAYOUT_REFUND) // + .recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) // + ); + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "07 March 2025", 915.88, 24.99, + 24, null); + Assertions.assertNotNull(loanId); + loanIdRef.set(loanId); + }); + + runAt("11 March 2025", () -> { + for (int i = 0; i < totalTransactions; i++) { + disburseLoan(loanIdRef.get(), BigDecimal.valueOf(228.97), "11 March 2025"); + } + }); + + runAt("04 April 2025", () -> { + executeInlineCOB(loanIdRef.get()); + Long response = loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "04 April 2025", 48.91).getResourceId(); + Assertions.assertNotNull(response); + }); + + runAt("02 May 2025", () -> { + executeInlineCOB(loanIdRef.get()); + Long response = loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "02 May 2025", 48.91).getResourceId(); + Assertions.assertNotNull(response); + }); + + runAt("30 May 2025", () -> { + executeInlineCOB(loanIdRef.get()); + Long response = loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "30 May 2025", 48.91).getResourceId(); + Assertions.assertNotNull(response); + }); + + runAt("27 June 2025", () -> { + executeInlineCOB(loanIdRef.get()); + Long response = loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "27 June 2025", 48.91).getResourceId(); + Assertions.assertNotNull(response); + }); + + runAt("08 August 2025", () -> { + executeInlineCOB(loanIdRef.get()); + Long response = loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "08 August 2025", 48.91).getResourceId(); + Assertions.assertNotNull(response); + }); + + runAt("05 September 2025", () -> { + executeInlineCOB(loanIdRef.get()); + Long response = loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "05 September 2025", 48.91) + .getResourceId(); + Assertions.assertNotNull(response); + }); + + runAt("03 October 2025", () -> { + executeInlineCOB(loanIdRef.get()); + Long response = loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "03 October 2025", 48.91).getResourceId(); + Assertions.assertNotNull(response); + }); + + runAt("08 October 2025", () -> { + executeInlineCOB(loanIdRef.get()); + for (int i = 0; i < totalTransactions; i++) { + final String transactionExternalId = UUID.randomUUID().toString(); + PostLoansLoanIdTransactionsResponse refundResponse = loanTransactionHelper.makeMerchantIssuedRefund(loanIdRef.get(), + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("08 October 2025") + .locale(LOCALE).transactionAmount(228.97).externalId(transactionExternalId) + .interestRefundCalculation(false)); + Assertions.assertNotNull(refundResponse.getResourceId()); + merchantIssuedRefundTransactions.set(i, refundResponse); + } + }); + + runAt("09 October 2025", () -> { + executeInlineCOB(loanIdRef.get()); + loanTransactionHelper.makeCreditBalanceRefund(loanIdRef.get(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("09 October 2025").locale(LOCALE).transactionAmount(225.15)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanIdRef.get()); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + + for (int i = 0; i < totalTransactions; i++) { + loanTransactionHelper.createManualInterestRefund(loanIdRef.get(), merchantIssuedRefundTransactions.get(i).getResourceId(), + null, 0.01, null); + } + }); + } + AdvancedPaymentData createPaymentAllocationInterestPrincipalPenaltyFee(String transactionType, String futureInstallmentAllocationRule) { AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); advancedPaymentData.setTransactionType(transactionType);
