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"));