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 5c8d86cba5 FINERACT-2394:Fixed Deposits is creating double Interest 
Postings
5c8d86cba5 is described below

commit 5c8d86cba5b656bc6161328ecf7628cbfc206dd7
Author: alburquerquerangelarturo <[email protected]>
AuthorDate: Thu Oct 16 19:38:37 2025 -0600

    FINERACT-2394:Fixed Deposits is creating double Interest Postings
---
 .../PostInterestForSavingTasklet.java              |  16 --
 .../SavingsInterestPostingTest.java                | 173 +++++++++++++--------
 2 files changed, 108 insertions(+), 81 deletions(-)

diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java
index f46c77df04..e579456ec3 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java
@@ -166,22 +166,6 @@ public class PostInterestForSavingTasklet implements 
Tasklet {
 
         List<Future<Void>> responses = new ArrayList<>();
         posters.forEach(poster -> responses.add(taskExecutor.submit(poster)));
-        Long maxId = maxSavingsIdInList;
-        if (!queue.isEmpty()) {
-            maxId = Math.max(maxSavingsIdInList, 
queue.element().get(queue.element().size() - 1).getId());
-        }
-
-        while (queue.size() <= QUEUE_SIZE) {
-            log.debug("Fetching while threads are running!..:: this is not 
supposed to run........");
-            savingsAccounts = 
Collections.synchronizedList(this.savingAccountReadPlatformService
-                    
.retrieveAllSavingsDataForInterestPosting(backdatedTxnsAllowedTill, pageSize, 
ACTIVE.getValue(), maxId));
-            if (savingsAccounts.isEmpty()) {
-                break;
-            }
-            maxId = savingsAccounts.get(savingsAccounts.size() - 1).getId();
-            log.debug("Add to the Queue");
-            queue.add(savingsAccounts);
-        }
 
         checkCompletion(responses);
         log.debug("Queue size {}", queue.size());
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
index 2e368f0c65..098b9a9624 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsInterestPostingTest.java
@@ -18,6 +18,8 @@
  */
 package org.apache.fineract.integrationtests;
 
+import static 
org.apache.fineract.integrationtests.common.BusinessDateHelper.runAt;
+
 import io.restassured.builder.RequestSpecBuilder;
 import io.restassured.builder.ResponseSpecBuilder;
 import io.restassured.http.ContentType;
@@ -34,11 +36,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-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.client.models.PostSavingsAccountsAccountIdRequest;
 import org.apache.fineract.infrastructure.core.service.MathUtil;
-import org.apache.fineract.integrationtests.common.BusinessDateHelper;
 import org.apache.fineract.integrationtests.common.ClientHelper;
 import org.apache.fineract.integrationtests.common.CommonConstants;
 import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
@@ -51,6 +50,7 @@ import 
org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
 import 
org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
 import 
org.apache.fineract.integrationtests.common.savings.SavingsTestLifecycleExtension;
 import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -88,9 +88,14 @@ public class SavingsInterestPostingTest {
         this.globalConfigurationHelper = new GlobalConfigurationHelper();
     }
 
+    @AfterEach
+    public void cleanupAfterTest() {
+        cleanupSavingsAccountsFromDuplicatePreventionTest();
+    }
+
     @Test
     public void testPostInterestWithOverdraftProduct() {
-        try {
+        runAt("12 March 2025", () -> {
             final String amount = "10000";
 
             final Account assetAccount = accountHelper.createAssetAccount();
@@ -115,13 +120,10 @@ public class SavingsInterestPostingTest {
             savingsAccountHelper.activateSavings(accountId, startDateString);
             savingsAccountHelper.depositToSavingsAccount(accountId, amount, 
startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
 
-            // Simulate time passing - update business date to March
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(true));
             LocalDate marchDate = LocalDate.of(2025, 3, 2);
-            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, 
BusinessDateType.BUSINESS_DATE, marchDate);
 
-            runAccrualsThenPost();
+            schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
+            schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
 
             long days = ChronoUnit.DAYS.between(startDate, 
marchDate.minusDays(1));
             BigDecimal expected = calcInterestPosting(productHelper, amount, 
days);
@@ -135,15 +137,12 @@ public class SavingsInterestPostingTest {
             Assertions.assertEquals(0L, overdraftCount, "Expected NO OVERDRAFT 
posting on posting date");
 
             assertNoAccrualReversals(accountId);
-        } finally {
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(false));
-        }
+        });
     }
 
     @Test
     public void testOverdraftInterestWithOverdraftProduct() {
-        try {
+        runAt("12 March 2025", () -> {
             final String amount = "10000";
 
             final Account assetAccount = accountHelper.createAssetAccount();
@@ -168,13 +167,10 @@ public class SavingsInterestPostingTest {
             savingsAccountHelper.activateSavings(accountId, startDateString);
             savingsAccountHelper.withdrawalFromSavingsAccount(accountId, 
amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
 
-            // Simulate time passing - update business date to March
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(true));
             LocalDate marchDate = LocalDate.of(2025, 3, 2);
-            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, 
BusinessDateType.BUSINESS_DATE, marchDate);
 
-            runAccrualsThenPost();
+            schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
+            schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
 
             long days = ChronoUnit.DAYS.between(startDate, 
marchDate.minusDays(1));
             BigDecimal expected = calcOverdraftPosting(productHelper, amount, 
days);
@@ -191,15 +187,12 @@ public class SavingsInterestPostingTest {
             Assertions.assertEquals(1L, overdraftCount, "Expected exactly one 
OVERDRAFT posting on posting date");
 
             assertNoAccrualReversals(accountId);
-        } finally {
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(false));
-        }
+        });
     }
 
     @Test
     public void 
testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceLessZero() {
-        try {
+        runAt("12 March 2025", () -> {
             final String amountDeposit = "10000";
             final String amountWithdrawal = "20000";
 
@@ -230,12 +223,10 @@ public class SavingsInterestPostingTest {
             savingsAccountHelper.withdrawalFromSavingsAccount(accountId, 
amountWithdrawal, withdrawalStr,
                     CommonConstants.RESPONSE_RESOURCE_ID);
 
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(true));
             LocalDate marchDate = LocalDate.of(2025, 3, 2);
-            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, 
BusinessDateType.BUSINESS_DATE, marchDate);
 
-            runAccrualsThenPost();
+            schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
+            schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
 
             List<HashMap> txs = getInterestTransactions(accountId);
             for (HashMap tx : txs) {
@@ -262,15 +253,12 @@ public class SavingsInterestPostingTest {
                     "Expected exactly one OVERDRAFT posting on posting date");
 
             assertNoAccrualReversals(accountId);
-        } finally {
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(false));
-        }
+        });
     }
 
     @Test
     public void 
testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceGreaterZero() {
-        try {
+        runAt("12 March 2025", () -> {
             final String amountDeposit = "20000";
             final String amountWithdrawal = "10000";
 
@@ -300,12 +288,10 @@ public class SavingsInterestPostingTest {
             final String depositStr = DateTimeFormatter.ofPattern("dd MMMM 
yyyy", Locale.US).format(depositDate);
             savingsAccountHelper.depositToSavingsAccount(accountId, 
amountDeposit, depositStr, CommonConstants.RESPONSE_RESOURCE_ID);
 
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(true));
             LocalDate marchDate = LocalDate.of(2025, 3, 2);
-            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, 
BusinessDateType.BUSINESS_DATE, marchDate);
 
-            runAccrualsThenPost();
+            schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
+            schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
 
             List<HashMap> txs = getInterestTransactions(accountId);
             for (HashMap tx : txs) {
@@ -332,15 +318,12 @@ public class SavingsInterestPostingTest {
                     "Expected exactly one INTEREST posting on posting date");
 
             assertNoAccrualReversals(accountId);
-        } finally {
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(false));
-        }
+        });
     }
 
     @Test
     public void testPostInterestNotZero() {
-        try {
+        runAt("12 March 2025", () -> {
             final String amountDeposit = "1000";
             final String amountWithdrawal = "1000";
 
@@ -366,20 +349,17 @@ public class SavingsInterestPostingTest {
             savingsAccountHelper.activateSavings(accountId, startStr);
             savingsAccountHelper.depositToSavingsAccount(accountId, 
amountDeposit, startStr, CommonConstants.RESPONSE_RESOURCE_ID);
 
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(true));
-            LocalDate februaryDate = LocalDate.of(startDate.getYear(), 2, 1);
-            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, 
BusinessDateType.BUSINESS_DATE, februaryDate);
+            LocalDate februaryDate = LocalDate.of(2025, 2, 1);
 
             schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
 
-            List<HashMap> txsFebruary = getInterestTransactions(accountId); // 
OBTENER EL POSTEO DEL INTEREST
+            List<HashMap> txsFebruary = getInterestTransactions(accountId);
 
             long daysFebruary = ChronoUnit.DAYS.between(startDate, 
februaryDate);
             BigDecimal expectedFebruary = calcInterestPosting(productHelper, 
amountDeposit, daysFebruary);
             Assertions.assertEquals(expectedFebruary, 
BigDecimal.valueOf(((Double) txsFebruary.get(0).get("amount"))));
 
-            final LocalDate withdrawalDate = LocalDate.of(startDate.getYear(), 
2, 1);
+            final LocalDate withdrawalDate = LocalDate.of(2025, 2, 1);
             final String withdrawal = DateTimeFormatter.ofPattern("dd MMMM 
yyyy", Locale.US).format(withdrawalDate);
 
             BigDecimal runningBalance = new 
BigDecimal(txsFebruary.get(0).get("runningBalance").toString());
@@ -390,13 +370,12 @@ public class SavingsInterestPostingTest {
             savingsAccountHelper.withdrawalFromSavingsAccount(accountId, 
amountWithdrawal, withdrawal,
                     CommonConstants.RESPONSE_RESOURCE_ID);
 
-            LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1);
-            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, 
BusinessDateType.BUSINESS_DATE, marchDate);
+            LocalDate marchDate = LocalDate.of(2025, 3, 1);
 
             schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
 
-            List<HashMap> txs = getInterestTransactions(accountId); // CON 
ESTE DEBEMOS DE VALIDAR QUE EL DIA DE MARZO
-                                                                    // NO SE 
TENGA POSTEO EN CERO
+            List<HashMap> txs = getInterestTransactions(accountId);
+
             for (HashMap tx : txs) {
                 BigDecimal amt = BigDecimal.valueOf(((Double) 
tx.get("amount")));
                 @SuppressWarnings("unchecked")
@@ -418,9 +397,82 @@ public class SavingsInterestPostingTest {
                     "Expected exactly one OVERDRAFT posting on posting date");
 
             assertNoAccrualReversals(accountId);
-        } finally {
-            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
-                    new PutGlobalConfigurationsRequest().enabled(false));
+        });
+    }
+
+    @Test
+    public void testPostInterestForDuplicatePrevention() {
+        runAt("18 March 2025", () -> {
+            final String amount = "10000";
+
+            final Account assetAccount = accountHelper.createAssetAccount();
+            final Account incomeAccount = accountHelper.createIncomeAccount();
+            final Account expenseAccount = 
accountHelper.createExpenseAccount();
+            final Account liabilityAccount = 
accountHelper.createLiabilityAccount();
+            final Account interestReceivableAccount = 
accountHelper.createAssetAccount("interestReceivableAccount");
+            final Account savingsControlAccount = 
accountHelper.createLiabilityAccount("Savings Control");
+            final Account interestPayableAccount = 
accountHelper.createLiabilityAccount("Interest Payable");
+
+            final Integer productId = 
createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(
+                    interestPayableAccount.getAccountID().toString(), 
savingsControlAccount.getAccountID().toString(),
+                    interestReceivableAccount.getAccountID().toString(), 
assetAccount, incomeAccount, expenseAccount, liabilityAccount);
+
+            final LocalDate startDate = LocalDate.of(2025, 2, 1);
+
+            List<Integer> accountIdList = new ArrayList<>();
+            for (int i = 0; i < 800; i++) {
+
+                final Integer clientId = 
ClientHelper.createClient(requestSpec, responseSpec, "01 January 2025");
+                final String startDateString = DateTimeFormatter.ofPattern("dd 
MMMM yyyy", Locale.US).format(startDate);
+                final Integer accountId = 
savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
+                        SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, 
startDateString);
+
+                savingsAccountHelper.approveSavingsOnDate(accountId, 
startDateString);
+                savingsAccountHelper.activateSavings(accountId, 
startDateString);
+                savingsAccountHelper.depositToSavingsAccount(accountId, 
amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
+
+                accountIdList.add(accountId);
+            }
+            Assertions.assertEquals(800, accountIdList.size(), "ERROR: 
Expected 800");
+
+            schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
+
+            for (Integer accountId : accountIdList) {
+                List<HashMap> txs = getInterestTransactions(accountId);
+                Assertions.assertEquals(1, txs.size(), "ERROR: Duplicate 
interest postings exist.");
+            }
+        });
+    }
+
+    private void cleanupSavingsAccountsFromDuplicatePreventionTest() {
+        try {
+            LOG.info("Starting cleanup of savings accounts after duplicate 
prevention test");
+
+            List<Long> savingsIds = 
SavingsAccountHelper.getSavingsIdsByStatusId(300);
+            if (!savingsIds.isEmpty()) {
+                LOG.info("Found {} savings accounts to cleanup", 
savingsIds.size());
+
+                savingsIds.forEach(savingsId -> {
+                    try {
+
+                        
savingsAccountHelper.postInterestForSavings(savingsId.intValue());
+
+                        savingsAccountHelper.closeSavingsAccount(savingsId,
+                                new 
PostSavingsAccountsAccountIdRequest().locale("en").dateFormat(Utils.DATE_FORMAT)
+                                        
.closedOnDate(Utils.dateFormatter.format(Utils.getLocalDateOfTenant())).withdrawBalance(true));
+
+                        LOG.debug("Savings account {} closed successfully", 
savingsId);
+                    } catch (Exception e) {
+                        LOG.warn("Unable to close savings account {}: {}", 
savingsId, e.getMessage());
+                    }
+                });
+
+                LOG.info("Savings accounts cleanup completed");
+            } else {
+                LOG.info("No savings accounts found to cleanup");
+            }
+        } catch (Exception e) {
+            LOG.error("Error during savings accounts cleanup: {}", 
e.getMessage(), e);
         }
     }
 
@@ -528,15 +580,6 @@ public class SavingsInterestPostingTest {
                 
.filter(SavingsAccountTransactionType::isOverDraftInterestPosting).count();
     }
 
-    private void runAccrualsThenPost() {
-        try {
-            schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
-        } catch (IllegalArgumentException ex) {
-            LOG.warn("Accruals job not found ({}). Continuing without it.", 
ACCRUALS_JOB_NAME, ex);
-        }
-        schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
-    }
-
     @SuppressWarnings({ "rawtypes" })
     private boolean isReversed(HashMap tx) {
         Object v = tx.get("reversed");

Reply via email to