blob: 098b9a96240330460190141263172584f5168c2f [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
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;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.fineract.client.models.PostSavingsAccountsAccountIdRequest;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
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;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ExtendWith({ SavingsTestLifecycleExtension.class })
public class SavingsInterestPostingTest {
private static final Logger LOG = LoggerFactory.getLogger(SavingsInterestPostingTest.class);
private static ResponseSpecification responseSpec;
private static RequestSpecification requestSpec;
private AccountHelper accountHelper;
private SavingsAccountHelper savingsAccountHelper;
private SchedulerJobHelper schedulerJobHelper;
public static final String MINIMUM_OPENING_BALANCE = "1000.0";
private GlobalConfigurationHelper globalConfigurationHelper;
private SavingsProductHelper productHelper;
private JournalEntryHelper journalEntryHelper;
private static final String ACCRUALS_JOB_NAME = "Add Accrual Transactions For Savings";
private static final String POST_INTEREST_JOB_NAME = "Post Interest For Savings";
@BeforeEach
public void setup() {
Utils.initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.schedulerJobHelper = new SchedulerJobHelper(this.requestSpec);
this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec);
this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, this.responseSpec);
this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
this.globalConfigurationHelper = new GlobalConfigurationHelper();
}
@AfterEach
public void cleanupAfterTest() {
cleanupSavingsAccountsFromDuplicatePreventionTest();
}
@Test
public void testPostInterestWithOverdraftProduct() {
runAt("12 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 Integer clientId = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2025");
final LocalDate startDate = LocalDate.of(2025, 2, 1);
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);
LocalDate marchDate = LocalDate.of(2025, 3, 2);
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);
List<HashMap> txs = getInterestTransactions(accountId);
Assertions.assertEquals(expected, BigDecimal.valueOf(((Double) txs.get(0).get("amount"))), "ERROR in expected");
long interestCount = countInterestOnDate(accountId, marchDate.minusDays(1));
long overdraftCount = countOverdraftOnDate(accountId, marchDate.minusDays(1));
Assertions.assertEquals(1L, interestCount, "Expected exactly one INTEREST posting on posting date");
Assertions.assertEquals(0L, overdraftCount, "Expected NO OVERDRAFT posting on posting date");
assertNoAccrualReversals(accountId);
});
}
@Test
public void testOverdraftInterestWithOverdraftProduct() {
runAt("12 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 Integer clientId = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2025");
final LocalDate startDate = LocalDate.of(2025, 2, 1);
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.withdrawalFromSavingsAccount(accountId, amount, startDateString, CommonConstants.RESPONSE_RESOURCE_ID);
LocalDate marchDate = LocalDate.of(2025, 3, 2);
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);
List<HashMap> txs = getInterestTransactions(accountId);
Assertions.assertEquals(expected, BigDecimal.valueOf(((Double) txs.get(0).get("amount"))));
BigDecimal runningBalance = BigDecimal.valueOf(((Double) txs.get(0).get("runningBalance")));
Assertions.assertTrue(MathUtil.isLessThanZero(runningBalance), "Running balance is not less than zero");
long interestCount = countInterestOnDate(accountId, marchDate.minusDays(1));
long overdraftCount = countOverdraftOnDate(accountId, marchDate.minusDays(1));
Assertions.assertEquals(0L, interestCount, "Expected NO INTEREST posting on posting date");
Assertions.assertEquals(1L, overdraftCount, "Expected exactly one OVERDRAFT posting on posting date");
assertNoAccrualReversals(accountId);
});
}
@Test
public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceLessZero() {
runAt("12 March 2025", () -> {
final String amountDeposit = "10000";
final String amountWithdrawal = "20000";
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 Integer clientId = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2025");
final LocalDate startDate = LocalDate.of(2025, 2, 1);
final String startStr = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(startDate);
final Integer accountId = savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startStr);
savingsAccountHelper.approveSavingsOnDate(accountId, startStr);
savingsAccountHelper.activateSavings(accountId, startStr);
savingsAccountHelper.depositToSavingsAccount(accountId, amountDeposit, startStr, CommonConstants.RESPONSE_RESOURCE_ID);
final LocalDate withdrawalDate = LocalDate.of(2025, 2, 16);
final String withdrawalStr = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(withdrawalDate);
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, withdrawalStr,
CommonConstants.RESPONSE_RESOURCE_ID);
LocalDate marchDate = LocalDate.of(2025, 3, 2);
schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
List<HashMap> txs = getInterestTransactions(accountId);
for (HashMap tx : txs) {
BigDecimal amt = BigDecimal.valueOf(((Double) tx.get("amount")));
@SuppressWarnings("unchecked")
Map<String, Object> typeMap = (Map<String, Object>) tx.get("transactionType");
SavingsAccountTransactionType type = SavingsAccountTransactionType.fromInt(((Double) typeMap.get("id")).intValue());
if (type.isInterestPosting()) {
long days = ChronoUnit.DAYS.between(startDate, withdrawalDate);
BigDecimal expected = calcInterestPosting(productHelper, amountDeposit, days);
Assertions.assertEquals(expected, amt);
} else {
long days = ChronoUnit.DAYS.between(withdrawalDate, marchDate.minusDays(1));
BigDecimal overdraftBase = new BigDecimal(amountWithdrawal).subtract(new BigDecimal(amountDeposit));
BigDecimal expected = calcOverdraftPosting(productHelper, overdraftBase.toString(), days);
Assertions.assertEquals(expected, amt);
}
}
Assertions.assertEquals(1L, countInterestOnDate(accountId, marchDate.minusDays(1)),
"Expected exactly one INTEREST posting on posting date");
Assertions.assertEquals(1L, countOverdraftOnDate(accountId, marchDate.minusDays(1)),
"Expected exactly one OVERDRAFT posting on posting date");
assertNoAccrualReversals(accountId);
});
}
@Test
public void testOverdraftAndInterestPosting_WithOverdraftProduct_WhitBalanceGreaterZero() {
runAt("12 March 2025", () -> {
final String amountDeposit = "20000";
final String amountWithdrawal = "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 Integer clientId = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2025");
final LocalDate startDate = LocalDate.of(2025, 2, 1);
final String startStr = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(startDate);
final Integer accountId = savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startStr);
savingsAccountHelper.approveSavingsOnDate(accountId, startStr);
savingsAccountHelper.activateSavings(accountId, startStr);
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, startStr, CommonConstants.RESPONSE_RESOURCE_ID);
final LocalDate depositDate = LocalDate.of(2025, 2, 16);
final String depositStr = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(depositDate);
savingsAccountHelper.depositToSavingsAccount(accountId, amountDeposit, depositStr, CommonConstants.RESPONSE_RESOURCE_ID);
LocalDate marchDate = LocalDate.of(2025, 3, 2);
schedulerJobHelper.executeAndAwaitJob(ACCRUALS_JOB_NAME);
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
List<HashMap> txs = getInterestTransactions(accountId);
for (HashMap tx : txs) {
BigDecimal amt = BigDecimal.valueOf(((Double) tx.get("amount")));
@SuppressWarnings("unchecked")
Map<String, Object> typeMap = (Map<String, Object>) tx.get("transactionType");
SavingsAccountTransactionType type = SavingsAccountTransactionType.fromInt(((Double) typeMap.get("id")).intValue());
if (type.isOverDraftInterestPosting()) {
long days = ChronoUnit.DAYS.between(startDate, depositDate);
BigDecimal expected = calcOverdraftPosting(productHelper, amountWithdrawal, days);
Assertions.assertEquals(expected, amt);
} else {
long days = ChronoUnit.DAYS.between(depositDate, marchDate.minusDays(1));
BigDecimal positiveBase = new BigDecimal(amountDeposit).subtract(new BigDecimal(amountWithdrawal));
BigDecimal expected = calcInterestPosting(productHelper, positiveBase.toString(), days);
Assertions.assertEquals(expected, amt);
}
}
Assertions.assertEquals(1L, countOverdraftOnDate(accountId, marchDate.minusDays(1)),
"Expected exactly one OVERDRAFT posting on posting date");
Assertions.assertEquals(1L, countInterestOnDate(accountId, marchDate.minusDays(1)),
"Expected exactly one INTEREST posting on posting date");
assertNoAccrualReversals(accountId);
});
}
@Test
public void testPostInterestNotZero() {
runAt("12 March 2025", () -> {
final String amountDeposit = "1000";
final String amountWithdrawal = "1000";
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 Integer clientId = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2025");
final LocalDate startDate = LocalDate.of(LocalDate.now(Utils.getZoneIdOfTenant()).getYear(), 1, 1);
final String startStr = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US).format(startDate);
final Integer accountId = savingsAccountHelper.applyForSavingsApplicationOnDate(clientId, productId,
SavingsAccountHelper.ACCOUNT_TYPE_INDIVIDUAL, startStr);
savingsAccountHelper.approveSavingsOnDate(accountId, startStr);
savingsAccountHelper.activateSavings(accountId, startStr);
savingsAccountHelper.depositToSavingsAccount(accountId, amountDeposit, startStr, CommonConstants.RESPONSE_RESOURCE_ID);
LocalDate februaryDate = LocalDate.of(2025, 2, 1);
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
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(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());
String withdrawalRunning = runningBalance.setScale(2, RoundingMode.HALF_UP).toString();
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, withdrawalRunning, withdrawal,
CommonConstants.RESPONSE_RESOURCE_ID);
savingsAccountHelper.withdrawalFromSavingsAccount(accountId, amountWithdrawal, withdrawal,
CommonConstants.RESPONSE_RESOURCE_ID);
LocalDate marchDate = LocalDate.of(2025, 3, 1);
schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);
List<HashMap> txs = getInterestTransactions(accountId);
for (HashMap tx : txs) {
BigDecimal amt = BigDecimal.valueOf(((Double) tx.get("amount")));
@SuppressWarnings("unchecked")
Map<String, Object> typeMap = (Map<String, Object>) tx.get("transactionType");
SavingsAccountTransactionType type = SavingsAccountTransactionType.fromInt(((Double) typeMap.get("id")).intValue());
if (type.isOverDraftInterestPosting()) {
long days = ChronoUnit.DAYS.between(withdrawalDate, marchDate);
BigDecimal decimalsss = new BigDecimal(txsFebruary.get(0).get("runningBalance").toString())
.subtract(runningBalance.setScale(2, RoundingMode.HALF_UP));
BigDecimal withdraw = new BigDecimal(amountWithdrawal);
BigDecimal res = withdraw.subtract(decimalsss);
BigDecimal expected = calcOverdraftPosting(productHelper, res.toString(), days);
Assertions.assertEquals(expected, amt);
}
}
Assertions.assertEquals(0L, countInterestOnDate(accountId, marchDate), "Expected exactly one INTEREST posting on posting date");
Assertions.assertEquals(1L, countOverdraftOnDate(accountId, marchDate),
"Expected exactly one OVERDRAFT posting on posting date");
assertNoAccrualReversals(accountId);
});
}
@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);
}
}
private List<HashMap> getInterestTransactions(Integer savingsAccountId) {
List<HashMap> all = savingsAccountHelper.getSavingsTransactions(savingsAccountId);
List<HashMap> filtered = new ArrayList<>();
for (HashMap tx : all) {
@SuppressWarnings("unchecked")
Map<String, Object> txType = (Map<String, Object>) tx.get("transactionType");
SavingsAccountTransactionType type = SavingsAccountTransactionType.fromInt(((Double) txType.get("id")).intValue());
if (type.isInterestPosting() || type.isOverDraftInterestPosting()) {
filtered.add(tx);
}
}
return filtered;
}
public Integer createSavingsProductWithAccrualAccountingWithOutOverdraftAllowed(final String interestPayableAccount,
final String savingsControlAccount, final String interestReceivableAccount, final Account... accounts) {
LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT WITHOUT OVERDRAFT ---------------------------------------");
this.productHelper = new SavingsProductHelper().withOverDraftRate("100000", "21")
.withAccountInterestReceivables(interestReceivableAccount).withSavingsControlAccountId(savingsControlAccount)
.withInterestPayableAccountId(interestPayableAccount).withInterestCompoundingPeriodTypeAsAnnually()
.withInterestPostingPeriodTypeAsMonthly().withInterestCalculationPeriodTypeAsDailyBalance()
.withAccountingRuleAsAccrualBased(accounts);
final String savingsProductJSON = this.productHelper.build();
return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec);
}
private BigDecimal calcInterestPosting(SavingsProductHelper productHelper, String amount, long days) {
BigDecimal rate = productHelper.getNominalAnnualInterestRate().divide(new BigDecimal("100.00"));
BigDecimal principal = new BigDecimal(amount);
BigDecimal dayFactor = BigDecimal.ONE.divide(productHelper.getInterestCalculationDaysInYearType(), MathContext.DECIMAL64);
BigDecimal dailyRate = rate.multiply(dayFactor, MathContext.DECIMAL64);
BigDecimal periodRate = dailyRate.multiply(BigDecimal.valueOf(days), MathContext.DECIMAL64);
return principal.multiply(periodRate, MathContext.DECIMAL64).setScale(productHelper.getDecimalCurrency(), RoundingMode.HALF_EVEN);
}
private BigDecimal calcOverdraftPosting(SavingsProductHelper productHelper, String amount, long days) {
BigDecimal rate = productHelper.getNominalAnnualInterestRateOverdraft().divide(new BigDecimal("100.00"));
BigDecimal principal = new BigDecimal(amount);
BigDecimal dayFactor = BigDecimal.ONE.divide(productHelper.getInterestCalculationDaysInYearType(), MathContext.DECIMAL64);
BigDecimal dailyRate = rate.multiply(dayFactor, MathContext.DECIMAL64);
BigDecimal periodRate = dailyRate.multiply(BigDecimal.valueOf(days), MathContext.DECIMAL64);
return principal.multiply(periodRate, MathContext.DECIMAL64).setScale(productHelper.getDecimalCurrency(), RoundingMode.HALF_EVEN);
}
@SuppressWarnings("unchecked")
private LocalDate coerceToLocalDate(HashMap tx) {
String[] candidateKeys = new String[] { "date", "transactionDate", "submittedOnDate", "createdDate" };
for (String key : candidateKeys) {
Object v = tx.get(key);
if (v == null) {
continue;
}
if (v instanceof List<?>) {
List<?> arr = (List<?>) v;
if (arr.size() >= 3 && arr.get(0) instanceof Number && arr.get(1) instanceof Number && arr.get(2) instanceof Number) {
int year = ((Number) arr.get(0)).intValue();
int month = ((Number) arr.get(1)).intValue();
int day = ((Number) arr.get(2)).intValue();
return LocalDate.of(year, month, day);
}
}
if (v instanceof String) {
String s = (String) v;
DateTimeFormatter[] fmts = new DateTimeFormatter[] { DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US),
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.US), DateTimeFormatter.ofPattern("yyyy-MM-dd") };
for (DateTimeFormatter f : fmts) {
try {
return LocalDate.parse(s, f);
} catch (Exception ignore) {
// intentionally ignored
}
}
}
}
return null;
}
private boolean isDate(HashMap tx, LocalDate expected) {
LocalDate got = coerceToLocalDate(tx);
return got != null && got.isEqual(expected);
}
@SuppressWarnings("unchecked")
private SavingsAccountTransactionType txType(HashMap tx) {
Map<String, Object> typeMap = (Map<String, Object>) tx.get("transactionType");
return SavingsAccountTransactionType.fromInt(((Double) typeMap.get("id")).intValue());
}
private long countInterestOnDate(Integer accountId, LocalDate date) {
List<HashMap> all = savingsAccountHelper.getSavingsTransactions(accountId);
return all.stream().filter(tx -> isDate(tx, date)).map(this::txType).filter(SavingsAccountTransactionType::isInterestPosting)
.count();
}
private long countOverdraftOnDate(Integer accountId, LocalDate date) {
List<HashMap> all = savingsAccountHelper.getSavingsTransactions(accountId);
return all.stream().filter(tx -> isDate(tx, date)).map(this::txType)
.filter(SavingsAccountTransactionType::isOverDraftInterestPosting).count();
}
@SuppressWarnings({ "rawtypes" })
private boolean isReversed(HashMap tx) {
Object v = tx.get("reversed");
if (v instanceof Boolean) {
return (Boolean) v;
}
if (v instanceof Number) {
return ((Number) v).intValue() != 0;
}
if (v instanceof String) {
return Boolean.parseBoolean((String) v);
}
return false;
}
@SuppressWarnings("rawtypes")
private void assertNoAccrualReversals(Integer accountId) {
List<HashMap> all = savingsAccountHelper.getSavingsTransactions(accountId);
long reversedAccruals = all.stream().filter(tx -> {
SavingsAccountTransactionType t = txType(tx);
return t.isAccrual() && isReversed(tx);
}).count();
Assertions.assertEquals(0L, reversedAccruals, "Accrual reversals were found in account transactions");
}
}