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