FINERACT-2394:Fixed Deposits is creating double Interest Postings

This commit is contained in:
alburquerquerangelarturo
2025-11-03 10:59:54 +01:00
committed by Adam Saghy
parent 8c3c44b618
commit 5c8d86cba5
2 changed files with 108 additions and 81 deletions
@@ -166,22 +166,6 @@ public class PostInterestForSavingTasklet implements Tasklet {
List<Future<Void>> responses = new ArrayList<>(); List<Future<Void>> responses = new ArrayList<>();
posters.forEach(poster -> responses.add(taskExecutor.submit(poster))); 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); checkCompletion(responses);
log.debug("Queue size {}", queue.size()); log.debug("Queue size {}", queue.size());
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.integrationtests; package org.apache.fineract.integrationtests;
import static org.apache.fineract.integrationtests.common.BusinessDateHelper.runAt;
import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder; import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
@@ -34,11 +36,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest; import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.infrastructure.core.service.MathUtil; 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.ClientHelper;
import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; 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.SavingsProductHelper;
import org.apache.fineract.integrationtests.common.savings.SavingsTestLifecycleExtension; import org.apache.fineract.integrationtests.common.savings.SavingsTestLifecycleExtension;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -88,9 +88,14 @@ public class SavingsInterestPostingTest {
this.globalConfigurationHelper = new GlobalConfigurationHelper(); this.globalConfigurationHelper = new GlobalConfigurationHelper();
} }
@AfterEach
public void cleanupAfterTest() {
cleanupSavingsAccountsFromDuplicatePreventionTest();
}
@Test @Test
public void testPostInterestWithOverdraftProduct() { public void testPostInterestWithOverdraftProduct() {
try { runAt("12 March 2025", () -> {
final String amount = "10000"; final String amount = "10000";
final Account assetAccount = accountHelper.createAssetAccount(); final Account assetAccount = accountHelper.createAssetAccount();
@@ -115,13 +120,10 @@ public class SavingsInterestPostingTest {
savingsAccountHelper.activateSavings(accountId, startDateString); savingsAccountHelper.activateSavings(accountId, startDateString);
savingsAccountHelper.depositToSavingsAccount(accountId, amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID); 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); 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)); long days = ChronoUnit.DAYS.between(startDate, marchDate.minusDays(1));
BigDecimal expected = calcInterestPosting(productHelper, amount, days); BigDecimal expected = calcInterestPosting(productHelper, amount, days);
@@ -135,15 +137,12 @@ public class SavingsInterestPostingTest {
Assertions.assertEquals(0L, overdraftCount, "Expected NO OVERDRAFT posting on posting date"); Assertions.assertEquals(0L, overdraftCount, "Expected NO OVERDRAFT posting on posting date");
assertNoAccrualReversals(accountId); assertNoAccrualReversals(accountId);
} finally { });
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
}
} }
@Test @Test
public void testOverdraftInterestWithOverdraftProduct() { public void testOverdraftInterestWithOverdraftProduct() {
try { runAt("12 March 2025", () -> {
final String amount = "10000"; final String amount = "10000";
final Account assetAccount = accountHelper.createAssetAccount(); final Account assetAccount = accountHelper.createAssetAccount();
@@ -168,13 +167,10 @@ public class SavingsInterestPostingTest {
savingsAccountHelper.activateSavings(accountId, startDateString); savingsAccountHelper.activateSavings(accountId, startDateString);
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID); 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); 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)); long days = ChronoUnit.DAYS.between(startDate, marchDate.minusDays(1));
BigDecimal expected = calcOverdraftPosting(productHelper, amount, days); 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"); Assertions.assertEquals(1L, overdraftCount, "Expected exactly one OVERDRAFT posting on posting date");
assertNoAccrualReversals(accountId); assertNoAccrualReversals(accountId);
} finally { });
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
}
} }
@Test @Test
public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceLessZero() { public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceLessZero() {
try { runAt("12 March 2025", () -> {
final String amountDeposit = "10000"; final String amountDeposit = "10000";
final String amountWithdrawal = "20000"; final String amountWithdrawal = "20000";
@@ -230,12 +223,10 @@ public class SavingsInterestPostingTest {
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, withdrawalStr, savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, withdrawalStr,
CommonConstants.RESPONSE_RESOURCE_ID); CommonConstants.RESPONSE_RESOURCE_ID);
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(true));
LocalDate marchDate = LocalDate.of(2025, 3, 2); 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); List<HashMap> txs = getInterestTransactions(accountId);
for (HashMap tx : txs) { for (HashMap tx : txs) {
@@ -262,15 +253,12 @@ public class SavingsInterestPostingTest {
"Expected exactly one OVERDRAFT posting on posting date"); "Expected exactly one OVERDRAFT posting on posting date");
assertNoAccrualReversals(accountId); assertNoAccrualReversals(accountId);
} finally { });
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
}
} }
@Test @Test
public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceGreaterZero() { public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceGreaterZero() {
try { runAt("12 March 2025", () -> {
final String amountDeposit = "20000"; final String amountDeposit = "20000";
final String amountWithdrawal = "10000"; final String amountWithdrawal = "10000";
@@ -300,12 +288,10 @@ public class SavingsInterestPostingTest {
final String depositStr = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(depositDate); final String depositStr = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(depositDate);
savingsAccountHelper.depositToSavingsAccount(accountId, amountDeposit, depositStr, CommonConstants.RESPONSE_RESOURCE_ID); 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); 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); List<HashMap> txs = getInterestTransactions(accountId);
for (HashMap tx : txs) { for (HashMap tx : txs) {
@@ -332,15 +318,12 @@ public class SavingsInterestPostingTest {
"Expected exactly one INTEREST posting on posting date"); "Expected exactly one INTEREST posting on posting date");
assertNoAccrualReversals(accountId); assertNoAccrualReversals(accountId);
} finally { });
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
new PutGlobalConfigurationsRequest().enabled(false));
}
} }
@Test @Test
public void testPostInterestNotZero() { public void testPostInterestNotZero() {
try { runAt("12 March 2025", () -> {
final String amountDeposit = "1000"; final String amountDeposit = "1000";
final String amountWithdrawal = "1000"; final String amountWithdrawal = "1000";
@@ -366,20 +349,17 @@ public class SavingsInterestPostingTest {
savingsAccountHelper.activateSavings(accountId, startStr); savingsAccountHelper.activateSavings(accountId, startStr);
savingsAccountHelper.depositToSavingsAccount(accountId, amountDeposit, startStr, CommonConstants.RESPONSE_RESOURCE_ID); savingsAccountHelper.depositToSavingsAccount(accountId, amountDeposit, startStr, CommonConstants.RESPONSE_RESOURCE_ID);
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE, LocalDate februaryDate = LocalDate.of(2025, 2, 1);
new PutGlobalConfigurationsRequest().enabled(true));
LocalDate februaryDate = LocalDate.of(startDate.getYear(), 2, 1);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, februaryDate);
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME); 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); long daysFebruary = ChronoUnit.DAYS.between(startDate, februaryDate);
BigDecimal expectedFebruary = calcInterestPosting(productHelper, amountDeposit, daysFebruary); BigDecimal expectedFebruary = calcInterestPosting(productHelper, amountDeposit, daysFebruary);
Assertions.assertEquals(expectedFebruary, BigDecimal.valueOf(((Double) txsFebruary.get(0).get("amount")))); 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); final String withdrawal = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(withdrawalDate);
BigDecimal runningBalance = new BigDecimal(txsFebruary.get(0).get("runningBalance").toString()); BigDecimal runningBalance = new BigDecimal(txsFebruary.get(0).get("runningBalance").toString());
@@ -390,13 +370,12 @@ public class SavingsInterestPostingTest {
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, withdrawal, savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, withdrawal,
CommonConstants.RESPONSE_RESOURCE_ID); CommonConstants.RESPONSE_RESOURCE_ID);
LocalDate marchDate = LocalDate.of(startDate.getYear(), 3, 1); LocalDate marchDate = LocalDate.of(2025, 3, 1);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, marchDate);
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME); schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
List<HashMap> txs = getInterestTransactions(accountId); // CON ESTE DEBEMOS DE VALIDAR QUE EL DIA DE MARZO List<HashMap> txs = getInterestTransactions(accountId);
// NO SE TENGA POSTEO EN CERO
for (HashMap tx : txs) { for (HashMap tx : txs) {
BigDecimal amt = BigDecimal.valueOf(((Double) tx.get("amount"))); BigDecimal amt = BigDecimal.valueOf(((Double) tx.get("amount")));
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -418,9 +397,82 @@ public class SavingsInterestPostingTest {
"Expected exactly one OVERDRAFT posting on posting date"); "Expected exactly one OVERDRAFT posting on posting date");
assertNoAccrualReversals(accountId); 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(); .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" }) @SuppressWarnings({ "rawtypes" })
private boolean isReversed(HashMap tx) { private boolean isReversed(HashMap tx) {
Object v = tx.get("reversed"); Object v = tx.get("reversed");