From 5c8d86cba5b656bc6161328ecf7628cbfc206dd7 Mon Sep 17 00:00:00 2001 From: alburquerquerangelarturo Date: Thu, 16 Oct 2025 19:38:37 -0600 Subject: [PATCH] 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 f46c77df0..e579456ec 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> 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 2e368f0c6..098b9a962 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 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 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 txsFebruary = getInterestTransactions(accountId); // OBTENER EL POSTEO DEL INTEREST + List 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 txs = getInterestTransactions(accountId); // CON ESTE DEBEMOS DE VALIDAR QUE EL DIA DE MARZO - // NO SE TENGA POSTEO EN CERO + List 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 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 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 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");