Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,25 @@ public class SavingsSchedularInterestPoster {
private Collection<SavingsAccountData> savingAccounts;
private boolean backdatedTxnsAllowedTill;

@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)
public void postInterest() throws JobExecutionException {
if (!savingAccounts.isEmpty()) {
List<Throwable> errors = new ArrayList<>();
LocalDate currentDate = DateUtils.getBusinessLocalDate();
for (SavingsAccountData savingsAccountData : savingAccounts) {
boolean postInterestAsOn = false;
LocalDate transactionDate = null;
try {
if (isInterestAlreadyPostedForPeriod(savingsAccountData, currentDate)) {
log.debug("Interest already posted for savings account {} up to date {}, skipping", savingsAccountData.getId(),
savingsAccountData.getSummary().getInterestPostedTillDate());
continue;
}
SavingsAccountData savingsAccountDataRet = savingsAccountWritePlatformService.postInterest(savingsAccountData,
postInterestAsOn, transactionDate, backdatedTxnsAllowedTill);
savingsAccountDataList.add(savingsAccountDataRet);
if (hasNewInterestTransactions(savingsAccountDataRet)) {
savingsAccountDataList.add(savingsAccountDataRet);
}
} catch (Exception e) {
errors.add(e);
}
Expand Down Expand Up @@ -109,7 +117,6 @@ private void batchUpdateJournalEntries(final List<SavingsAccountData> savingsAcc
for (SavingsAccountTransactionData savingsAccountTransactionData : savingsAccountTransactionDataList) {
if (savingsAccountTransactionData.getId() == null && !MathUtil.isZero(savingsAccountTransactionData.getAmount())) {
final String key = savingsAccountTransactionData.getRefNo();
final Boolean isOverdraft = savingsAccountTransactionData.getIsOverdraft();
final SavingsAccountTransactionData dataFromFetch = savingsAccountTransactionDataHashMap.get(key);
savingsAccountTransactionData.setId(dataFromFetch.getId());
if (savingsAccountData.getGlAccountIdForSavingsControl() != 0
Expand Down Expand Up @@ -248,4 +255,20 @@ private String batchQueryForTransactionsUpdate() {
+ "SET is_reversed=?, amount=?, overdraft_amount_derived=?, balance_end_date_derived=?, balance_number_of_days_derived=?, running_balance_derived=?, cumulative_balance_derived=?, is_reversal=?, "
+ LAST_MODIFIED_DATE_DB_FIELD + " = ?, " + LAST_MODIFIED_BY_DB_FIELD + " = ? " + "WHERE id=?";
}

private boolean isInterestAlreadyPostedForPeriod(SavingsAccountData savingsAccountData, LocalDate currentDate) {
LocalDate interestPostedTillDate = savingsAccountData.getSummary().getInterestPostedTillDate();
if (interestPostedTillDate == null) {
return false;
}
return !interestPostedTillDate.isBefore(currentDate);
}

private boolean hasNewInterestTransactions(SavingsAccountData savingsAccountData) {
if (savingsAccountData.getSavingsAccountTransactionData() == null) {
return false;
}
return savingsAccountData.getSavingsAccountTransactionData().stream()
.anyMatch(tx -> tx.getId() == null && !MathUtil.isZero(tx.getAmount()) && tx.isInterestPosting());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,104 @@ public void testPostInterestForDuplicatePrevention() {
});
}

@Test
public void testPostInterestPreventsDuplicateOnSameDay() {
runAt("15 April 2025", () -> {
final String amount = "5000";

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, 3, 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);

schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);

List<HashMap> txsAfterFirstRun = getInterestTransactions(accountId);
Assertions.assertEquals(1, txsAfterFirstRun.size(), "Expected exactly one interest transaction after first job run");

HashMap summaryAfterFirstRun = savingsAccountHelper.getSavingsSummary(accountId);
BigDecimal balanceAfterFirstRun = BigDecimal.valueOf(((Double) summaryAfterFirstRun.get("accountBalance")));

schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);

List<HashMap> txsAfterSecondRun = getInterestTransactions(accountId);
Assertions.assertEquals(1, txsAfterSecondRun.size(),
"Expected still only one interest transaction after second job run on same day - duplicate should be prevented");

HashMap summaryAfterSecondRun = savingsAccountHelper.getSavingsSummary(accountId);
BigDecimal balanceAfterSecondRun = BigDecimal.valueOf(((Double) summaryAfterSecondRun.get("accountBalance")));

Assertions.assertEquals(balanceAfterFirstRun, balanceAfterSecondRun,
"Account balance should remain unchanged after second job run - no duplicate interest should be posted");
});
}

@Test
public void testPostInterestSkipsCurrentPeriodButAllowsNewPeriod() {
runAt("20 May 2025", () -> {
final String amount = "8000";

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, 4, 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);

schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);

List<HashMap> txsAfterFirstPeriod = getInterestTransactions(accountId);
Assertions.assertEquals(1, txsAfterFirstPeriod.size(),
"Expected exactly one interest transaction after first posting period");

HashMap summaryAfterFirstPeriod = savingsAccountHelper.getSavingsSummary(accountId);
BigDecimal balanceAfterFirstPeriod = BigDecimal.valueOf(((Double) summaryAfterFirstPeriod.get("accountBalance")));

schedulerJobHelper.executeAndAwaitJob(POST_INTEREST_JOB_NAME);

List<HashMap> txsAfterSecondRunSameDay = getInterestTransactions(accountId);
Assertions.assertEquals(1, txsAfterSecondRunSameDay.size(),
"Expected still only one interest transaction after second run on same day - current period should be skipped");

HashMap summaryAfterSecondRun = savingsAccountHelper.getSavingsSummary(accountId);
BigDecimal balanceAfterSecondRun = BigDecimal.valueOf(((Double) summaryAfterSecondRun.get("accountBalance")));
Assertions.assertEquals(balanceAfterFirstPeriod, balanceAfterSecondRun,
"Account balance should remain unchanged after second run on same day - current period posting should be skipped");
});
}

private void cleanupSavingsAccountsFromDuplicatePreventionTest() {
try {
LOG.info("Starting cleanup of savings accounts after duplicate prevention test");
Expand Down
Loading