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 fe2b4b60473..a0b17cfe892 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 @@ -1412,19 +1412,24 @@ public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Lo new LoanAdjustTransactionBusinessEvent(new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust))); if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { throw new PlatformServiceUnavailableException("error.msg.loan.transfer.transaction.update.not.allowed", - "Loan transaction:" + transactionId + " update not allowed as it involves in account transfer", transactionId); + "Loan transaction: " + transactionId + " update not allowed as it involves in account transfer", transactionId); } if (loan.isClosedWrittenOff()) { throw new PlatformServiceUnavailableException("error.msg.loan.written.off.update.not.allowed", - "Loan transaction:" + transactionId + " update not allowed as loan status is written off", transactionId); + "Loan transaction: " + transactionId + " update not allowed as loan status is written off", transactionId); } if (transactionToAdjust.hasChargebackLoanTransactionRelations()) { throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed", - "Loan transaction:" + transactionId + " update not allowed as loan transaction is linked to other transactions", + "Loan transaction: " + transactionId + " update not allowed as loan transaction is linked to other transactions", transactionId); } + if (transactionToAdjust.isInterestRefund()) { + throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed", + "Interest refund transaction: " + transactionId + " cannot be reversed or adjusted directly", transactionId); + } + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); 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 f3c75450827..91578eb6241 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 @@ -19,6 +19,9 @@ package org.apache.fineract.integrationtests; 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; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; @@ -29,13 +32,17 @@ import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; +import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.Utils; @@ -1193,6 +1200,64 @@ public void verifyUC18S2() { }); } + // UC19: Interest Refund reverse transaction only when the related transactions, Merchant Issued Refund or Payout + // Refund are reversed + // 1. Create a Loan Product that supports Interest Refund Types + // 2. Submit, Approve and Disburse the loan + // 3. Apply a Merchant Issued Refund Transaction + // 4. Try to reverse the Interest Refund Transaction expecting to have an Exception + // 5. Reverse the Merchant Issued Refund transaction and review the Interest Refund Transction is reversed too + @Test + public void verifyUC19() { + AtomicReference loanIdRef = new AtomicReference<>(); + runAt("1 January 2021", () -> { + PostLoanProductsResponse loanProduct = loanProductHelper + .createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .multiDisburseLoan(true)// + .disallowExpectedDisbursements(true)// + .maxTrancheCount(2)// + .addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.PAYOUT_REFUND) // + .addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) // + .recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) // + ); + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.9, + 12, null); + Assertions.assertNotNull(loanId); + loanIdRef.set(loanId); + disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021"); + }); + runAt("22 January 2021", () -> { + Long loanId = loanIdRef.get(); + loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", "22 January 2021", 1000F, loanId.intValue()); + logLoanTransactions(loanId); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); + Optional optInterestRefundTransaction = loanDetails.getTransactions().stream() + .filter(item -> Objects.equals(item.getType().getValue(), "Interest Refund")).findFirst(); + final Long interestRefundTransactionId = optInterestRefundTransaction.get().getId(); + + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.reverseLoanTransaction(loanId, interestRefundTransactionId, + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("22 January 2021").transactionAmount(0.0).locale("en"))); + assertEquals(503, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.transaction.update.not.allowed")); + + Optional optMerchantIssuedTransaction = loanDetails.getTransactions().stream() + .filter(item -> Objects.equals(item.getType().getValue(), "Merchant Issued Refund")).findFirst(); + final Long merchantIssuedTransactionId = optMerchantIssuedTransaction.get().getId(); + + loanTransactionHelper.reverseLoanTransaction(loanId, merchantIssuedTransactionId, + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("22 January 2021") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); + optInterestRefundTransaction = loanDetails.getTransactions().stream() + .filter(item -> Objects.equals(item.getType().getValue(), "Interest Refund")).findFirst(); + assertEquals(Boolean.TRUE, optInterestRefundTransaction.get().getManuallyReversed()); + }); + } + private void logInstallmentsOfLoanDetails(GetLoansLoanIdResponse loanDetails) { log.info("index, dueDate, principal, fee, penalty, interest"); if (loanDetails != null && loanDetails.getRepaymentSchedule() != null && loanDetails.getRepaymentSchedule().getPeriods() != null) {