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


The following commit(s) were added to refs/heads/develop by this push:
     new 8fe9dbc614 FINERACT-2327: when Interest Refund was created manually, 
we should only return the newly created Interest Refund transaction identifiers 
in the result.
8fe9dbc614 is described below

commit 8fe9dbc614bd86c408627a0d964211c8a77df820
Author: Attila Budai <[email protected]>
AuthorDate: Tue Aug 19 07:23:17 2025 +0200

    FINERACT-2327: when Interest Refund was created manually, we should only 
return the newly created Interest Refund transaction identifiers in the result.
---
 .../test/messaging/event/EventCheckHelper.java     |   3 +-
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |   6 +-
 ...nManualInterestRefundResponseStructureTest.java | 246 +++++++++++++++++++++
 .../common/loans/LoanTransactionHelper.java        |   6 +
 4 files changed, 255 insertions(+), 6 deletions(-)

diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
index d3f876f008..9b1c390d38 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java
@@ -309,8 +309,7 @@ public class EventCheckHelper {
             Response<PostLoansLoanIdTransactionsResponse> transactionResponse, 
TransactionType transactionType, String externalOwnerId)
             throws IOException {
         Long loanId = transactionResponse.body().getLoanId();
-        Long transactionId = 
transactionType.equals(TransactionType.INTEREST_REFUND) ? 
transactionResponse.body().getSubResourceId()
-                : transactionResponse.body().getResourceId();
+        Long transactionId = transactionResponse.body().getResourceId();
         Response<GetLoansLoanIdResponse> loanDetailsResponse = 
loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
         List<GetLoansLoanIdTransactions> transactions = 
loanDetailsResponse.body().getTransactions();
         GetLoansLoanIdTransactions transactionFound = transactions//
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 6594caad56..a9d4348ed0 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
@@ -3034,10 +3034,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl 
implements LoanWritePlatf
         return new CommandProcessingResultBuilder() //
                 .withCommandId(command.commandId()) //
                 .withLoanId(loan.getId()) //
-                .withEntityId(targetTransaction.getId()) //
-                .withEntityExternalId(targetTransaction.getExternalId()) //
-                .withSubEntityId(interestRefundTxn.getId()) //
-                .withSubEntityExternalId(interestRefundTxn.getExternalId()) //
+                .withEntityId(interestRefundTxn.getId()) //
+                .withEntityExternalId(interestRefundTxn.getExternalId()) //
                 .withOfficeId(loan.getOfficeId()) //
                 .withClientId(loan.getClientId()) //
                 .withGroupId(loan.getGroupId()) //
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanManualInterestRefundResponseStructureTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanManualInterestRefundResponseStructureTest.java
new file mode 100644
index 0000000000..fe69233498
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanManualInterestRefundResponseStructureTest.java
@@ -0,0 +1,246 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.util.UUID;
+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.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.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests validate that manual Interest Refund transactions return the correct 
response structure: - entityId should
+ * contain the Interest Refund transaction ID - entityExternalId should 
contain the Interest Refund external ID -
+ * subEntityId should be null/not set - subEntityExternalId should be null/not 
set
+ */
+@Slf4j
+public class LoanManualInterestRefundResponseStructureTest extends 
BaseLoanIntegrationTest {
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private LoanTransactionHelper loanTransactionHelper;
+    private PostClientsResponse client;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new 
ResponseSpecBuilder().expectStatusCode(200).build();
+        this.loanTransactionHelper = new LoanTransactionHelper(requestSpec, 
responseSpec);
+        this.client = 
ClientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+    }
+
+    @Test
+    public void testManualInterestRefundResponseStructureWithoutExternalIds() {
+        AtomicReference<Long> loanIdRef = new AtomicReference<>();
+        AtomicReference<Long> targetTransactionIdRef = new AtomicReference<>();
+
+        runAt("01 January 2024", () -> {
+            // Create loan product that supports manual interest refund
+            PostLoanProductsResponse loanProduct = loanProductHelper
+                    
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.ACTUAL)
+                            
.addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND)
+                            
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY));
+
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProduct.getResourceId(), "01 January 2024", 1000.0, 9.9,
+                    12, null);
+            assertNotNull(loanId);
+            loanIdRef.set(loanId);
+
+            disburseLoan(loanId, BigDecimal.valueOf(1000), "01 January 2024");
+        });
+
+        runAt("15 January 2024", () -> {
+            Long loanId = loanIdRef.get();
+
+            // Make a merchant issued refund to have a target transaction that 
supports manual interest refund
+            PostLoansLoanIdTransactionsResponse refundResponse = 
makeLoanMerchantIssuedRefund(loanId, "15 January 2024", 100.0);
+            assertNotNull(refundResponse);
+            assertNotNull(refundResponse.getResourceId());
+            targetTransactionIdRef.set(refundResponse.getResourceId());
+
+            // Create manual interest refund via API
+            PostLoansLoanIdTransactionsResponse interestRefundResponse = 
createManualInterestRefund(loanId, refundResponse.getResourceId(),
+                    "15 January 2024", 5.0, null);
+
+            assertNotNull(interestRefundResponse, "Interest refund response 
should not be null");
+            assertNotNull(interestRefundResponse.getResourceId(), "Interest 
refund resource ID should not be null");
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            GetLoansLoanIdTransactions interestRefundTransaction = 
findTransactionByType(loanDetails, "Interest Refund");
+            assertNotNull(interestRefundTransaction, "Interest Refund 
transaction should exist");
+
+            assertEquals(interestRefundTransaction.getId(), 
interestRefundResponse.getResourceId(),
+                    "Response entityId should be the Interest Refund 
transaction ID");
+
+            // entityExternalId should be null (since no external ID was 
provided)
+            assertNull(interestRefundResponse.getResourceExternalId(), 
"entityExternalId should be null when no external ID provided");
+
+            // subEntityId should be null (not the target transaction ID)
+            assertNull(interestRefundResponse.getSubResourceId(), "subEntityId 
should be null");
+
+            // subEntityExternalId should be null
+            assertNull(interestRefundResponse.getSubResourceExternalId(), 
"subEntityExternalId should be null");
+        });
+    }
+
+    @Test
+    public void testManualInterestRefundResponseStructureWithExternalIds() {
+        AtomicReference<String> loanExternalIdRef = new AtomicReference<>();
+        AtomicReference<Long> loanIdRef = new AtomicReference<>();
+        AtomicReference<String> targetTransactionExternalIdRef = new 
AtomicReference<>();
+
+        String loanExternalId = UUID.randomUUID().toString();
+        loanExternalIdRef.set(loanExternalId);
+
+        runAt("01 February 2024", () -> {
+            // Create loan product that supports manual interest refund
+            PostLoanProductsResponse loanProduct = loanProductHelper
+                    
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.ACTUAL)
+                            
.addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND)
+                            
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY));
+
+            Long loanId = 
applyAndApproveProgressiveLoanWithExternalId(client.getClientId(), 
loanProduct.getResourceId(), loanExternalId,
+                    "01 February 2024", 1000.0, 9.9, 12, null);
+            assertNotNull(loanId);
+            loanIdRef.set(loanId);
+
+            disburseLoan(loanId, BigDecimal.valueOf(1000), "01 February 2024");
+        });
+
+        runAt("15 February 2024", () -> {
+            Long loanId = loanIdRef.get();
+            String repaymentExternalId = UUID.randomUUID().toString();
+            targetTransactionExternalIdRef.set(repaymentExternalId);
+
+            // Make a merchant issued refund with external ID (without 
automatic interest refund)
+            PostLoansLoanIdTransactionsResponse refundResponse = 
makeLoanMerchantIssuedRefundWithExternalId(loanId, repaymentExternalId,
+                    "15 February 2024", 100.0);
+            assertNotNull(refundResponse);
+            assertNotNull(refundResponse.getResourceId());
+
+            // Create manual interest refund with external ID
+            String interestRefundExternalId = UUID.randomUUID().toString();
+            PostLoansLoanIdTransactionsResponse interestRefundResponse = 
createManualInterestRefund(loanId, refundResponse.getResourceId(),
+                    "15 February 2024", 5.0, interestRefundExternalId);
+
+            assertNotNull(interestRefundResponse, "Interest refund response 
should not be null");
+            assertNotNull(interestRefundResponse.getResourceId(), "Interest 
refund resource ID should not be null");
+
+            // Get the actual interest refund transaction to verify
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            GetLoansLoanIdTransactions interestRefundTransaction = 
findTransactionByType(loanDetails, "Interest Refund");
+            assertNotNull(interestRefundTransaction, "Interest Refund 
transaction should exist");
+
+            assertEquals(interestRefundTransaction.getId(), 
interestRefundResponse.getResourceId(),
+                    "Response entityId should be the Interest Refund 
transaction ID");
+
+            assertEquals(interestRefundExternalId, 
interestRefundResponse.getResourceExternalId(),
+                    "entityExternalId should be the Interest Refund external 
ID");
+
+            assertNull(interestRefundResponse.getSubResourceId(), "subEntityId 
should be null");
+
+            assertNull(interestRefundResponse.getSubResourceExternalId(), 
"subEntityExternalId should be null");
+        });
+    }
+
+    /**
+     * 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)
+     */
+    private PostLoansLoanIdTransactionsResponse 
makeLoanMerchantIssuedRefund(Long loanId, String transactionDate, Double 
amount) {
+        // Create merchant issued refund transaction without automatic 
interest refund
+        org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest 
request = new 
org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest()
+                
.transactionDate(transactionDate).transactionAmount(amount).interestRefundCalculation(false).dateFormat("dd
 MMMM yyyy")
+                .locale("en");
+        return loanTransactionHelper.makeMerchantIssuedRefund(loanId, request);
+    }
+
+    /**
+     * Helper method to make loan merchant issued refund with external ID 
(without automatic interest refund)
+     */
+    private PostLoansLoanIdTransactionsResponse 
makeLoanMerchantIssuedRefundWithExternalId(Long loanId, String externalId,
+            String transactionDate, Double amount) {
+        // Create merchant issued refund transaction with external ID but 
without automatic interest refund
+        org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest 
request = new 
org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest()
+                
.transactionDate(transactionDate).transactionAmount(amount).externalId(externalId).interestRefundCalculation(false)
+                .dateFormat("dd MMMM yyyy").locale("en");
+        return loanTransactionHelper.makeMerchantIssuedRefund(loanId, request);
+    }
+
+    /**
+     * Helper method to find transaction by type
+     */
+    private GetLoansLoanIdTransactions 
findTransactionByType(GetLoansLoanIdResponse loanDetails, String 
transactionType) {
+        return loanDetails.getTransactions().stream().filter(t -> 
transactionType.equals(t.getType().getValue())).findFirst().orElse(null);
+    }
+
+    /**
+     * Helper method to apply and approve progressive loan with external ID
+     */
+    private Long applyAndApproveProgressiveLoanWithExternalId(Long clientId, 
Long productId, String loanExternalId, String submittedDate,
+            Double amount, Double interestRate, Integer termFrequency,
+            
java.util.function.Consumer<org.apache.fineract.client.models.PostLoansRequest> 
customizer) {
+
+        org.apache.fineract.client.models.PostLoansRequest request = 
applyLP2ProgressiveLoanRequest(clientId, productId, submittedDate,
+                amount, interestRate, termFrequency, customizer);
+        request.externalId(loanExternalId);
+
+        org.apache.fineract.client.models.PostLoansResponse loanResponse = 
loanTransactionHelper.applyLoan(request);
+        Long loanId = loanResponse.getLoanId();
+
+        loanTransactionHelper.approveLoan(loanId, approveLoanRequest(amount, 
submittedDate));
+        return loanId;
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index 8893082295..39a75fcce7 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -1181,6 +1181,12 @@ public class LoanTransactionHelper {
                 
FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction1(loanExternalId,
 request, "chargeRefund"));
     }
 
+    public PostLoansLoanIdTransactionsResponse manualInterestRefund(final Long 
loanId, final Long targetTransactionId,
+            final PostLoansLoanIdTransactionsTransactionIdRequest request) {
+        return 
Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction(loanId,
 targetTransactionId,
+                request, "interest-refund"));
+    }
+
     public PostLoansLoanIdTransactionsResponse makeGoodwillCredit(final Long 
loanId, final PostLoansLoanIdTransactionsRequest request) {
         return Calls
                 
.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId,
 request, "goodwillCredit"));

Reply via email to