From 13c79bfee4d767d39028a5199ef746ed628d3c90 Mon Sep 17 00:00:00 2001
From: Andrii Kulminskyi <kulminsky@gmail.com>
Date: Wed, 8 Jan 2025 18:05:14 +0200
Subject: [PATCH] FINERACT-1971: external_id column of accrual activity
 transaction gets updated to null when an accrual activity is reversed

---
 ...nAccrualActivityProcessingServiceImpl.java |   2 +-
 .../RepaymentReverseExternalIdTest.java       | 148 ++++++++++++++++++
 2 files changed, 149 insertions(+), 1 deletion(-)
 create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/RepaymentReverseExternalIdTest.java

diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java
index 2f237396696..ddc18312083 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java
@@ -204,7 +204,7 @@ private boolean validateActivityTransaction(@NotNull LoanRepaymentScheduleInstal
 
     private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) {
         loanTransaction.reverse();
-        loanTransaction.updateExternalId(null);
+
         LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
         businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
     }
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RepaymentReverseExternalIdTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RepaymentReverseExternalIdTest.java
new file mode 100644
index 00000000000..df936e13846
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RepaymentReverseExternalIdTest.java
@@ -0,0 +1,148 @@
+/**
+ * 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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+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.time.LocalDate;
+import java.util.HashMap;
+import java.util.UUID;
+import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+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.PutGlobalConfigurationsRequest;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
+import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class RepaymentReverseExternalIdTest extends BaseLoanIntegrationTest {
+
+    private static final String loanAmount = "1000";
+    private static final String startDate = "20 December 2024";
+    private static final String firstRepaymentDate = "23 December 2024";
+    private static final String secondRepaymentDate = "26 December 2024";
+    private static final String reverseDate = "27 December 2024";
+    private static final Double firstRepaymentAmount = 1000.0;
+    private static final Double secondRepaymentAmount = 10.0;
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private ClientHelper clientHelper;
+    private LoanTransactionHelper loanTransactionHelper;
+
+    @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(this.requestSpec, this.responseSpec);
+        this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec);
+    }
+
+    @Test
+    public void testReverseRepaymentUpdatesExternalIdCorrectlyForOverpayment() {
+        try {
+            // Set up the business date if required
+            final LocalDate todaysDate = Utils.getLocalDateOfTenant();
+            globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+                    new PutGlobalConfigurationsRequest().enabled(true));
+            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, todaysDate);
+
+            // Create a client and a loan with externalId
+            final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue();
+            final GetLoanProductsProductIdResponse loanProduct = createLoanProduct(loanTransactionHelper, null);
+            final String loanExternalId = UUID.randomUUID().toString();
+            final Integer loanId = createLoanAccount(clientId, loanProduct.getId(), loanExternalId);
+
+            // First repayment to cover part of the loan
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction1 = loanTransactionHelper.makeLoanRepayment(loanExternalId,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(firstRepaymentDate).locale("en")
+                            .transactionAmount(firstRepaymentAmount));
+
+            // Second repayment to create overpayment
+            final PostLoansLoanIdTransactionsResponse repaymentTransaction2 = loanTransactionHelper.makeLoanRepayment(loanExternalId,
+                    new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate(secondRepaymentDate).locale("en")
+                            .transactionAmount(secondRepaymentAmount)); // This creates an overpayment as total is now
+                                                                        // 1010
+
+            // Verify that the loan is marked as overpaid
+            GetLoansLoanIdResponse loanDetailsOverpaid = loanTransactionHelper.getLoanDetails((long) loanId);
+            assertTrue(loanDetailsOverpaid.getStatus().getOverpaid()); // Overpaid status should be true
+            assertNotNull(loanDetailsOverpaid.getOverpaidOnDate()); // Overpaid date should be set
+            assertEquals(loanDetailsOverpaid.getExternalId(), loanExternalId); // externalId should match the original
+
+            // Reverse the second repayment to remove the overpayment
+            loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction2.getResourceId().intValue(), reverseDate);
+
+            // Verify that the loan is no longer overpaid and overpaid date is reset
+            GetLoansLoanIdResponse loanDetailsNotOverpaid = loanTransactionHelper.getLoanDetails((long) loanId);
+            assertFalse(loanDetailsNotOverpaid.getStatus().getOverpaid()); // Overpaid status should be false
+            assertNull(loanDetailsNotOverpaid.getOverpaidOnDate()); // Overpaid date should be reset
+            assertEquals(loanDetailsNotOverpaid.getExternalId(), loanExternalId); // externalId should remain unchanged
+
+        } finally {
+            // Disable business date configuration if it was enabled
+            globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
+                    new PutGlobalConfigurationsRequest().enabled(false));
+        }
+    }
+
+    private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper,
+            final Integer delinquencyBucketId) {
+        final HashMap<String, Object> loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId);
+        final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap));
+        return loanTransactionHelper.getLoanProduct(loanProductId);
+    }
+
+    private Integer createLoanAccount(final Integer clientID, final Long loanProductID, final String externalId) {
+
+        String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(loanAmount).withLoanTermFrequency("1")
+                .withLoanTermFrequencyAsMonths().withNumberOfRepayments("1").withRepaymentEveryAfter("1")
+                .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance()
+                .withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod()
+                .withExpectedDisbursementDate(startDate).withSubmittedOnDate(startDate).withLoanType("individual")
+                .withExternalId(externalId).build(clientID.toString(), loanProductID.toString(), null);
+
+        final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON);
+        loanTransactionHelper.approveLoan(startDate, loanAmount, loanId, null);
+        loanTransactionHelper.disburseLoan(Long.valueOf(loanId), new PostLoansLoanIdRequest().actualDisbursementDate(startDate)
+                .transactionAmount(new BigDecimal(loanAmount)).locale("en").dateFormat("dd MMMM yyyy"));
+        return loanId;
+    }
+}