diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 45ecfe5a6..f8c57ab36 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -2686,6 +2686,19 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { transactionForAdjustment.reverse(reversalExternalId); transactionForAdjustment.manuallyAdjustedOrReversed(); + if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.MERCHANT_ISSUED_REFUND) + || transactionForAdjustment.getTypeOf().equals(LoanTransactionType.PAYOUT_REFUND)) { + getLoanTransactions().stream() // + .filter(LoanTransaction::isNotReversed) + .filter(loanTransaction -> loanTransaction.getLoanTransactionRelations().stream() + .anyMatch(relation -> relation.getRelationType().equals(LoanTransactionRelationTypeEnum.RELATED) + && relation.getToTransaction().getId().equals(transactionForAdjustment.getId()))) + .forEach(loanTransaction -> { + loanTransaction.reverse(); + loanTransaction.manuallyAdjustedOrReversed(); + }); + } + if (isClosedWrittenOff()) { // find write off transaction and reverse it final LoanTransaction writeOffTransaction = findWriteOffTransaction(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java index f49e29a3b..52cbec85e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java @@ -23,7 +23,8 @@ public enum LoanTransactionRelationTypeEnum { INVALID(0, "loanTransactionType.invalid"), // CHARGEBACK(1, "loanTransactionRelationType.chargeback"), // CHARGE_ADJUSTMENT(2, "loanTransactionRelationType.chargeAdjustment"), // - REPLAYED(3, "loanTransactionRelationType.replayed"); + REPLAYED(3, "loanTransactionRelationType.replayed"), // + RELATED(4, "loanTransactionRelationType.related"); private final Integer value; private final String code; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index ce5332e85..dcbba6ed0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -868,6 +868,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { if (shouldCreateInterestRefundTransaction) { interestRefundTransaction = createInterestRefundLoanTransaction(loan, transactionDate, transactionAmount); + if (interestRefundTransaction != null) { + interestRefundTransaction.getLoanTransactionRelations().add(LoanTransactionRelation + .linkToTransaction(interestRefundTransaction, refundTransaction, LoanTransactionRelationTypeEnum.RELATED)); + } } final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java index 29ac1ce4c..95819058d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java @@ -29,12 +29,15 @@ import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.atomic.AtomicLong; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.integrationtests.common.BusinessDateHelper; @@ -44,6 +47,7 @@ import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.AccountHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -226,6 +230,68 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest { }); } + @Test + public void testMerchantIssuedRefundCreatesAndReversesInterestRefund() { + runAt("01 July 2024", () -> { + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + final Long loanId = createLoanForMerchantIssuedRefundWithInterestRefund(clientId); + final PostLoansLoanIdTransactionsResponse merchantIssuedRefundResponse = loanTransactionHelper.makeMerchantIssuedRefund(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("01 July 2024").locale("en") + .transactionAmount(100.0)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertTrue( + loanDetails.getTransactions().stream().anyMatch(transaction -> transaction.getType().getMerchantIssuedRefund() + && Boolean.FALSE.equals(transaction.getManuallyReversed()))); + + Assertions.assertTrue(loanDetails.getTransactions().stream() + .anyMatch(transaction -> transaction.getType().getCode().equals("loanTransactionType.interestRefund") + && Boolean.FALSE.equals(transaction.getManuallyReversed()))); + + loanTransactionHelper.reverseLoanTransaction(loanId.intValue(), merchantIssuedRefundResponse.getResourceId(), "01 July 2024", + responseSpec); + + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertTrue( + loanDetails.getTransactions().stream().anyMatch(transaction -> transaction.getType().getMerchantIssuedRefund() + && Boolean.TRUE.equals(transaction.getManuallyReversed()))); + + Assertions.assertTrue(loanDetails.getTransactions().stream() + .anyMatch(transaction -> transaction.getType().getCode().equals("loanTransactionType.interestRefund") + && Boolean.TRUE.equals(transaction.getManuallyReversed()))); + }); + } + + @Test + public void testPayoutRefundCreatesAndReversesInterestRefund() { + runAt("01 July 2024", () -> { + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + final Long loanId = createLoanForPayoutRefundWithInterestRefund(clientId); + final PostLoansLoanIdTransactionsResponse payoutRefundResponse = loanTransactionHelper.makePayoutRefund(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("01 July 2024").locale("en") + .transactionAmount(100.0)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( + transaction -> transaction.getType().getPayoutRefund() && Boolean.FALSE.equals(transaction.getManuallyReversed()))); + + Assertions.assertTrue(loanDetails.getTransactions().stream() + .anyMatch(transaction -> transaction.getType().getCode().equals("loanTransactionType.interestRefund") + && Boolean.FALSE.equals(transaction.getManuallyReversed()))); + + loanTransactionHelper.reverseLoanTransaction(loanId.intValue(), payoutRefundResponse.getResourceId(), "01 July 2024", + responseSpec); + + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( + transaction -> transaction.getType().getPayoutRefund() && Boolean.TRUE.equals(transaction.getManuallyReversed()))); + + Assertions.assertTrue(loanDetails.getTransactions().stream() + .anyMatch(transaction -> transaction.getType().getCode().equals("loanTransactionType.interestRefund") + && Boolean.TRUE.equals(transaction.getManuallyReversed()))); + }); + } + private String buildJsonBody(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges, final BigDecimal penaltyCharges, final BigDecimal overpayment) { final HashMap map = new HashMap<>(); @@ -240,4 +306,22 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest { return new Gson().toJson(map); } + private Long createLoanForMerchantIssuedRefundWithInterestRefund(Long clientId) { + return createLoanForRefundWithInterestRefund(clientId, "MERCHANT_ISSUED_REFUND"); + } + + private Long createLoanForPayoutRefundWithInterestRefund(Long clientId) { + return createLoanForRefundWithInterestRefund(clientId, "PAYOUT_REFUND"); + } + + private Long createLoanForRefundWithInterestRefund(Long clientId, String refundType) { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct( + create4IProgressive().supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem(refundType)); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan( + applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null)); + Long loanId = postLoansResponse.getLoanId(); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024")); + disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 June 2024"); + return loanId; + } }