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